]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - stand/lua/menu.lua
Add UPDATING entries and bump version.
[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.resetfg()
51         else
52                 return str .. color.escapefg(color.RED) .. "off" ..
53                     color.resetfg()
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                 local multi_user = menu_entries.multi_user
216                 local single_user = menu_entries.single_user
217                 local boot_entry_1, boot_entry_2
218                 if core.isSingleUserBoot() then
219                         -- Swap the first two menu items on single user boot.
220                         -- We'll cache the alternate entries for performance.
221                         local alts = menu_entries.alts
222                         if alts == nil then
223                                 single_user = core.deepCopyTable(single_user)
224                                 multi_user = core.deepCopyTable(multi_user)
225                                 single_user.name = single_user.alternate_name
226                                 multi_user.name = multi_user.alternate_name
227                                 menu_entries.alts = {
228                                         single_user = single_user,
229                                         multi_user = multi_user,
230                                 }
231                         else
232                                 single_user = alts.single_user 
233                                 multi_user = alts.multi_user
234                         end
235                         boot_entry_1, boot_entry_2 = single_user, multi_user
236                 else
237                         boot_entry_1, boot_entry_2 = multi_user, single_user
238                 end
239                 return {
240                         boot_entry_1,
241                         boot_entry_2,
242                         menu_entries.prompt,
243                         menu_entries.reboot,
244                         menu_entries.console,
245                         {
246                                 entry_type = core.MENU_SEPARATOR,
247                         },
248                         {
249                                 entry_type = core.MENU_SEPARATOR,
250                                 name = "Options:",
251                         },
252                         menu_entries.kernel_options,
253                         menu_entries.boot_options,
254                         menu_entries.boot_envs,
255                         menu_entries.chainload,
256                 }
257         end,
258         all_entries = {
259                 multi_user = {
260                         entry_type = core.MENU_ENTRY,
261                         name = color.highlight("B") .. "oot Multi user " ..
262                             color.highlight("[Enter]"),
263                         -- Not a standard menu entry function!
264                         alternate_name = color.highlight("B") ..
265                             "oot Multi user",
266                         func = function()
267                                 core.setSingleUser(false)
268                                 core.boot()
269                         end,
270                         alias = {"b", "B"},
271                 },
272                 single_user = {
273                         entry_type = core.MENU_ENTRY,
274                         name = "Boot " .. color.highlight("S") .. "ingle user",
275                         -- Not a standard menu entry function!
276                         alternate_name = "Boot " .. color.highlight("S") ..
277                             "ingle user " .. color.highlight("[Enter]"),
278                         func = function()
279                                 core.setSingleUser(true)
280                                 core.boot()
281                         end,
282                         alias = {"s", "S"},
283                 },
284                 console = {
285                         entry_type = core.MENU_ENTRY,
286                         name = function()
287                                 return color.highlight("C") .. "ons: " .. core.getConsoleName()
288                         end,
289                         func = function()
290                                 core.nextConsoleChoice()
291                         end,
292                         alias = {"c", "C"},
293                 },
294                 prompt = {
295                         entry_type = core.MENU_RETURN,
296                         name = color.highlight("Esc") .. "ape to loader prompt",
297                         func = function()
298                                 loader.setenv("autoboot_delay", "NO")
299                         end,
300                         alias = {core.KEYSTR_ESCAPE},
301                 },
302                 reboot = {
303                         entry_type = core.MENU_ENTRY,
304                         name = color.highlight("R") .. "eboot",
305                         func = function()
306                                 loader.perform("reboot")
307                         end,
308                         alias = {"r", "R"},
309                 },
310                 kernel_options = {
311                         entry_type = core.MENU_CAROUSEL_ENTRY,
312                         carousel_id = "kernel",
313                         items = core.kernelList,
314                         name = function(idx, choice, all_choices)
315                                 if #all_choices == 0 then
316                                         return "Kernel: "
317                                 end
318
319                                 local is_default = (idx == 1)
320                                 local kernel_name = ""
321                                 local name_color
322                                 if is_default then
323                                         name_color = color.escapefg(color.GREEN)
324                                         kernel_name = "default/"
325                                 else
326                                         name_color = color.escapefg(color.BLUE)
327                                 end
328                                 kernel_name = kernel_name .. name_color ..
329                                     choice .. color.resetfg()
330                                 return color.highlight("K") .. "ernel: " ..
331                                     kernel_name .. " (" .. idx .. " of " ..
332                                     #all_choices .. ")"
333                         end,
334                         func = function(_, choice, _)
335                                 if loader.getenv("kernelname") ~= nil then
336                                         loader.perform("unload")
337                                 end
338                                 config.selectKernel(choice)
339                         end,
340                         alias = {"k", "K"},
341                 },
342                 boot_options = {
343                         entry_type = core.MENU_SUBMENU,
344                         name = "Boot " .. color.highlight("O") .. "ptions",
345                         submenu = menu.boot_options,
346                         alias = {"o", "O"},
347                 },
348                 boot_envs = {
349                         entry_type = core.MENU_SUBMENU,
350                         visible = function()
351                                 return core.isZFSBoot() and
352                                     #core.bootenvList() > 1
353                         end,
354                         name = "Boot " .. color.highlight("E") .. "nvironments",
355                         submenu = menu.boot_environments,
356                         alias = {"e", "E"},
357                 },
358                 chainload = {
359                         entry_type = core.MENU_ENTRY,
360                         name = function()
361                                 return 'Chain' .. color.highlight("L") ..
362                                     "oad " .. loader.getenv('chain_disk')
363                         end,
364                         func = function()
365                                 loader.perform("chain " ..
366                                     loader.getenv('chain_disk'))
367                         end,
368                         visible = function()
369                                 return loader.getenv('chain_disk') ~= nil
370                         end,
371                         alias = {"l", "L"},
372                 },
373         },
374 }
375
376 menu.default = menu.welcome
377 -- current_alias_table will be used to keep our alias table consistent across
378 -- screen redraws, instead of relying on whatever triggered the redraw to update
379 -- the local alias_table in menu.process.
380 menu.current_alias_table = {}
381
382 function menu.draw(menudef)
383         -- Clear the screen, reset the cursor, then draw
384         screen.clear()
385         menu.current_alias_table = drawer.drawscreen(menudef)
386         drawn_menu = menudef
387         screen.defcursor()
388 end
389
390 -- 'keypress' allows the caller to indicate that a key has been pressed that we
391 -- should process as our initial input.
392 function menu.process(menudef, keypress)
393         assert(menudef ~= nil)
394
395         if drawn_menu ~= menudef then
396                 menu.draw(menudef)
397         end
398
399         while true do
400                 local key = keypress or io.getchar()
401                 keypress = nil
402
403                 -- Special key behaviors
404                 if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and
405                     menudef ~= menu.default then
406                         break
407                 elseif key == core.KEY_ENTER then
408                         core.boot()
409                         -- Should not return.  If it does, escape menu handling
410                         -- and drop to loader prompt.
411                         return false
412                 end
413
414                 key = string.char(key)
415                 -- check to see if key is an alias
416                 local sel_entry = nil
417                 for k, v in pairs(menu.current_alias_table) do
418                         if key == k then
419                                 sel_entry = v
420                                 break
421                         end
422                 end
423
424                 -- if we have an alias do the assigned action:
425                 if sel_entry ~= nil then
426                         local handler = menu.handlers[sel_entry.entry_type]
427                         assert(handler ~= nil)
428                         -- The handler's return value indicates if we
429                         -- need to exit this menu.  An omitted or true
430                         -- return value means to continue.
431                         if handler(menudef, sel_entry) == false then
432                                 return
433                         end
434                         -- If we got an alias key the screen is out of date...
435                         -- redraw it.
436                         menu.draw(menudef)
437                 end
438         end
439 end
440
441 function menu.run()
442         local autoboot_key
443         local delay = loader.getenv("autoboot_delay")
444
445         if delay ~= nil and delay:lower() == "no" then
446                 delay = nil
447         else
448                 delay = tonumber(delay) or 10
449         end
450
451         if delay == -1 then
452                 core.boot()
453                 return
454         end
455
456         menu.draw(menu.default)
457
458         if delay ~= nil then
459                 autoboot_key = menu.autoboot(delay)
460
461                 -- autoboot_key should return the key pressed.  It will only
462                 -- return nil if we hit the timeout and executed the timeout
463                 -- command.  Bail out.
464                 if autoboot_key == nil then
465                         return
466                 end
467         end
468
469         menu.process(menu.default, autoboot_key)
470         drawn_menu = nil
471
472         screen.defcursor()
473         print("Exiting menu!")
474 end
475
476 function menu.autoboot(delay)
477         local x = loader.getenv("loader_menu_timeout_x") or 4
478         local y = loader.getenv("loader_menu_timeout_y") or 23
479         local endtime = loader.time() + delay
480         local time
481         local last
482         repeat
483                 time = endtime - loader.time()
484                 if last == nil or last ~= time then
485                         last = time
486                         screen.setcursor(x, y)
487                         print("Autoboot in " .. time ..
488                             " seconds. [Space] to pause")
489                         screen.defcursor()
490                 end
491                 if io.ischar() then
492                         local ch = io.getchar()
493                         if ch == core.KEY_ENTER then
494                                 break
495                         else
496                                 -- erase autoboot msg
497                                 screen.setcursor(0, y)
498                                 print(string.rep(" ", 80))
499                                 screen.defcursor()
500                                 return ch
501                         end
502                 end
503
504                 loader.delay(50000)
505         until time <= 0
506
507         local cmd = loader.getenv("menu_timeout_command") or "boot"
508         cli_execute_unparsed(cmd)
509         return nil
510 end
511
512 -- CLI commands
513 function cli.menu()
514         menu.run()
515 end
516
517 return menu