]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - stand/lua/menu.lua
zfs: merge openzfs/zfs@ad0a55461
[FreeBSD/FreeBSD.git] / stand / lua / menu.lua
1 --
2 -- SPDX-License-Identifier: BSD-2-Clause
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         if loader.getenv("kernelname") ~= nil then
62                 loader.perform("unload")
63         end
64 end
65
66 local function multiUserPrompt()
67         return loader.getenv("loader_menu_multi_user_prompt") or "Multi user"
68 end
69
70 -- Module exports
71 menu.handlers = {
72         -- Menu handlers take the current menu and selected entry as parameters,
73         -- and should return a boolean indicating whether execution should
74         -- continue or not. The return value may be omitted if this entry should
75         -- have no bearing on whether we continue or not, indicating that we
76         -- should just continue after execution.
77         [core.MENU_ENTRY] = function(_, entry)
78                 -- run function
79                 entry.func()
80         end,
81         [core.MENU_CAROUSEL_ENTRY] = function(_, entry)
82                 -- carousel (rotating) functionality
83                 local carid = entry.carousel_id
84                 local caridx = config.getCarouselIndex(carid)
85                 local choices = entry.items
86                 if type(choices) == "function" then
87                         choices = choices()
88                 end
89                 if #choices > 0 then
90                         caridx = (caridx % #choices) + 1
91                         config.setCarouselIndex(carid, caridx)
92                         entry.func(caridx, choices[caridx], choices)
93                 end
94         end,
95         [core.MENU_SUBMENU] = function(_, entry)
96                 menu.process(entry.submenu)
97         end,
98         [core.MENU_RETURN] = function(_, entry)
99                 -- allow entry to have a function/side effect
100                 if entry.func ~= nil then
101                         entry.func()
102                 end
103                 return false
104         end,
105 }
106 -- loader menu tree is rooted at menu.welcome
107
108 menu.boot_environments = {
109         entries = {
110                 -- return to welcome menu
111                 return_menu_entry,
112                 {
113                         entry_type = core.MENU_CAROUSEL_ENTRY,
114                         carousel_id = "be_active",
115                         items = core.bootenvList,
116                         name = function(idx, choice, all_choices)
117                                 if #all_choices == 0 then
118                                         return "Active: "
119                                 end
120
121                                 local is_default = (idx == 1)
122                                 local bootenv_name = ""
123                                 local name_color
124                                 if is_default then
125                                         name_color = color.escapefg(color.GREEN)
126                                 else
127                                         name_color = color.escapefg(color.BLUE)
128                                 end
129                                 bootenv_name = bootenv_name .. name_color ..
130                                     choice .. color.resetfg()
131                                 return color.highlight("A").."ctive: " ..
132                                     bootenv_name .. " (" .. idx .. " of " ..
133                                     #all_choices .. ")"
134                         end,
135                         func = function(_, choice, _)
136                                 bootenvSet(choice)
137                         end,
138                         alias = {"a", "A"},
139                 },
140                 {
141                         entry_type = core.MENU_ENTRY,
142                         visible = function()
143                                 return core.isRewinded() == false
144                         end,
145                         name = function()
146                                 return color.highlight("b") .. "ootfs: " ..
147                                     core.bootenvDefault()
148                         end,
149                         func = function()
150                                 -- Reset active boot environment to the default
151                                 config.setCarouselIndex("be_active", 1)
152                                 bootenvSet(core.bootenvDefault())
153                         end,
154                         alias = {"b", "B"},
155                 },
156         },
157 }
158
159 menu.boot_options = {
160         entries = {
161                 -- return to welcome menu
162                 return_menu_entry,
163                 -- load defaults
164                 {
165                         entry_type = core.MENU_ENTRY,
166                         name = "Load System " .. color.highlight("D") ..
167                             "efaults",
168                         func = core.setDefaults,
169                         alias = {"d", "D"},
170                 },
171                 {
172                         entry_type = core.MENU_SEPARATOR,
173                 },
174                 {
175                         entry_type = core.MENU_SEPARATOR,
176                         name = "Boot Options:",
177                 },
178                 -- acpi
179                 {
180                         entry_type = core.MENU_ENTRY,
181                         visible = core.isSystem386,
182                         name = function()
183                                 return OnOff(color.highlight("A") ..
184                                     "CPI       :", core.acpi)
185                         end,
186                         func = core.setACPI,
187                         alias = {"a", "A"},
188                 },
189                 -- safe mode
190                 {
191                         entry_type = core.MENU_ENTRY,
192                         name = function()
193                                 return OnOff("Safe " .. color.highlight("M") ..
194                                     "ode  :", core.sm)
195                         end,
196                         func = core.setSafeMode,
197                         alias = {"m", "M"},
198                 },
199                 -- single user
200                 {
201                         entry_type = core.MENU_ENTRY,
202                         name = function()
203                                 return OnOff(color.highlight("S") ..
204                                     "ingle user:", core.su)
205                         end,
206                         func = core.setSingleUser,
207                         alias = {"s", "S"},
208                 },
209                 -- verbose boot
210                 {
211                         entry_type = core.MENU_ENTRY,
212                         name = function()
213                                 return OnOff(color.highlight("V") ..
214                                     "erbose    :", core.verbose)
215                         end,
216                         func = core.setVerbose,
217                         alias = {"v", "V"},
218                 },
219         },
220 }
221
222 menu.welcome = {
223         entries = function()
224                 local menu_entries = menu.welcome.all_entries
225                 local multi_user = menu_entries.multi_user
226                 local single_user = menu_entries.single_user
227                 local boot_entry_1, boot_entry_2
228                 if core.isSingleUserBoot() then
229                         -- Swap the first two menu items on single user boot.
230                         -- We'll cache the alternate entries for performance.
231                         local alts = menu_entries.alts
232                         if alts == nil then
233                                 single_user = core.deepCopyTable(single_user)
234                                 multi_user = core.deepCopyTable(multi_user)
235                                 single_user.name = single_user.alternate_name
236                                 multi_user.name = multi_user.alternate_name
237                                 menu_entries.alts = {
238                                         single_user = single_user,
239                                         multi_user = multi_user,
240                                 }
241                         else
242                                 single_user = alts.single_user
243                                 multi_user = alts.multi_user
244                         end
245                         boot_entry_1, boot_entry_2 = single_user, multi_user
246                 else
247                         boot_entry_1, boot_entry_2 = multi_user, single_user
248                 end
249                 return {
250                         boot_entry_1,
251                         boot_entry_2,
252                         menu_entries.prompt,
253                         menu_entries.reboot,
254                         menu_entries.console,
255                         {
256                                 entry_type = core.MENU_SEPARATOR,
257                         },
258                         {
259                                 entry_type = core.MENU_SEPARATOR,
260                                 name = "Options:",
261                         },
262                         menu_entries.kernel_options,
263                         menu_entries.boot_options,
264                         menu_entries.zpool_checkpoints,
265                         menu_entries.boot_envs,
266                         menu_entries.chainload,
267                         menu_entries.vendor,
268                 }
269         end,
270         all_entries = {
271                 multi_user = {
272                         entry_type = core.MENU_ENTRY,
273                         name = function()
274                                 return color.highlight("B") .. "oot " ..
275                                     multiUserPrompt() .. " " ..
276                                     color.highlight("[Enter]")
277                         end,
278                         -- Not a standard menu entry function!
279                         alternate_name = function()
280                                 return color.highlight("B") .. "oot " ..
281                                     multiUserPrompt()
282                         end,
283                         func = function()
284                                 core.setSingleUser(false)
285                                 core.boot()
286                         end,
287                         alias = {"b", "B"},
288                 },
289                 single_user = {
290                         entry_type = core.MENU_ENTRY,
291                         name = "Boot " .. color.highlight("S") .. "ingle user",
292                         -- Not a standard menu entry function!
293                         alternate_name = "Boot " .. color.highlight("S") ..
294                             "ingle user " .. color.highlight("[Enter]"),
295                         func = function()
296                                 core.setSingleUser(true)
297                                 core.boot()
298                         end,
299                         alias = {"s", "S"},
300                 },
301                 console = {
302                         entry_type = core.MENU_ENTRY,
303                         name = function()
304                                 return color.highlight("C") .. "ons: " .. core.getConsoleName()
305                         end,
306                         func = function()
307                                 core.nextConsoleChoice()
308                         end,
309                         alias = {"c", "C"},
310                 },
311                 prompt = {
312                         entry_type = core.MENU_RETURN,
313                         name = color.highlight("Esc") .. "ape to loader prompt",
314                         func = function()
315                                 loader.setenv("autoboot_delay", "NO")
316                         end,
317                         alias = {core.KEYSTR_ESCAPE},
318                 },
319                 reboot = {
320                         entry_type = core.MENU_ENTRY,
321                         name = color.highlight("R") .. "eboot",
322                         func = function()
323                                 loader.perform("reboot")
324                         end,
325                         alias = {"r", "R"},
326                 },
327                 kernel_options = {
328                         entry_type = core.MENU_CAROUSEL_ENTRY,
329                         carousel_id = "kernel",
330                         items = core.kernelList,
331                         name = function(idx, choice, all_choices)
332                                 if #all_choices == 0 then
333                                         return "Kernel: "
334                                 end
335
336                                 local is_default = (idx == 1)
337                                 local kernel_name = ""
338                                 local name_color
339                                 if is_default then
340                                         name_color = color.escapefg(color.GREEN)
341                                         kernel_name = "default/"
342                                 else
343                                         name_color = color.escapefg(color.BLUE)
344                                 end
345                                 kernel_name = kernel_name .. name_color ..
346                                     choice .. color.resetfg()
347                                 return color.highlight("K") .. "ernel: " ..
348                                     kernel_name .. " (" .. idx .. " of " ..
349                                     #all_choices .. ")"
350                         end,
351                         func = function(_, choice, _)
352                                 if loader.getenv("kernelname") ~= nil then
353                                         loader.perform("unload")
354                                 end
355                                 config.selectKernel(choice)
356                         end,
357                         alias = {"k", "K"},
358                 },
359                 boot_options = {
360                         entry_type = core.MENU_SUBMENU,
361                         name = "Boot " .. color.highlight("O") .. "ptions",
362                         submenu = menu.boot_options,
363                         alias = {"o", "O"},
364                 },
365                 zpool_checkpoints = {
366                         entry_type = core.MENU_ENTRY,
367                         name = function()
368                                 local rewind = "No"
369                                 if core.isRewinded() then
370                                         rewind = "Yes"
371                                 end
372                                 return "Rewind ZFS " .. color.highlight("C") ..
373                                         "heckpoint: " .. rewind
374                         end,
375                         func = function()
376                                 core.changeRewindCheckpoint()
377                                 if core.isRewinded() then
378                                         bootenvSet(
379                                             core.bootenvDefaultRewinded())
380                                 else
381                                         bootenvSet(core.bootenvDefault())
382                                 end
383                                 config.setCarouselIndex("be_active", 1)
384                         end,
385                         visible = function()
386                                 return core.isZFSBoot() and
387                                     core.isCheckpointed()
388                         end,
389                         alias = {"c", "C"},
390                 },
391                 boot_envs = {
392                         entry_type = core.MENU_SUBMENU,
393                         visible = function()
394                                 return core.isZFSBoot() and
395                                     #core.bootenvList() > 1
396                         end,
397                         name = "Boot " .. color.highlight("E") .. "nvironments",
398                         submenu = menu.boot_environments,
399                         alias = {"e", "E"},
400                 },
401                 chainload = {
402                         entry_type = core.MENU_ENTRY,
403                         name = function()
404                                 return 'Chain' .. color.highlight("L") ..
405                                     "oad " .. loader.getenv('chain_disk')
406                         end,
407                         func = function()
408                                 loader.perform("chain " ..
409                                     loader.getenv('chain_disk'))
410                         end,
411                         visible = function()
412                                 return loader.getenv('chain_disk') ~= nil
413                         end,
414                         alias = {"l", "L"},
415                 },
416                 vendor = {
417                         entry_type = core.MENU_ENTRY,
418                         visible = function()
419                                 return false
420                         end
421                 },
422         },
423 }
424
425 menu.default = menu.welcome
426 -- current_alias_table will be used to keep our alias table consistent across
427 -- screen redraws, instead of relying on whatever triggered the redraw to update
428 -- the local alias_table in menu.process.
429 menu.current_alias_table = {}
430
431 function menu.draw(menudef)
432         -- Clear the screen, reset the cursor, then draw
433         screen.clear()
434         menu.current_alias_table = drawer.drawscreen(menudef)
435         drawn_menu = menudef
436         screen.defcursor()
437 end
438
439 -- 'keypress' allows the caller to indicate that a key has been pressed that we
440 -- should process as our initial input.
441 function menu.process(menudef, keypress)
442         assert(menudef ~= nil)
443
444         if drawn_menu ~= menudef then
445                 menu.draw(menudef)
446         end
447
448         while true do
449                 local key = keypress or io.getchar()
450                 keypress = nil
451
452                 -- Special key behaviors
453                 if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and
454                     menudef ~= menu.default then
455                         break
456                 elseif key == core.KEY_ENTER then
457                         core.boot()
458                         -- Should not return.  If it does, escape menu handling
459                         -- and drop to loader prompt.
460                         return false
461                 end
462
463                 key = string.char(key)
464                 -- check to see if key is an alias
465                 local sel_entry = nil
466                 for k, v in pairs(menu.current_alias_table) do
467                         if key == k then
468                                 sel_entry = v
469                                 break
470                         end
471                 end
472
473                 -- if we have an alias do the assigned action:
474                 if sel_entry ~= nil then
475                         local handler = menu.handlers[sel_entry.entry_type]
476                         assert(handler ~= nil)
477                         -- The handler's return value indicates if we
478                         -- need to exit this menu.  An omitted or true
479                         -- return value means to continue.
480                         if handler(menudef, sel_entry) == false then
481                                 return
482                         end
483                         -- If we got an alias key the screen is out of date...
484                         -- redraw it.
485                         menu.draw(menudef)
486                 end
487         end
488 end
489
490 function menu.run()
491         local autoboot_key
492         local delay = loader.getenv("autoboot_delay")
493
494         if delay ~= nil and delay:lower() == "no" then
495                 delay = nil
496         else
497                 delay = tonumber(delay) or 10
498         end
499
500         if delay == -1 then
501                 core.boot()
502                 return
503         end
504
505         menu.draw(menu.default)
506
507         if delay ~= nil then
508                 autoboot_key = menu.autoboot(delay)
509
510                 -- autoboot_key should return the key pressed.  It will only
511                 -- return nil if we hit the timeout and executed the timeout
512                 -- command.  Bail out.
513                 if autoboot_key == nil then
514                         return
515                 end
516         end
517
518         menu.process(menu.default, autoboot_key)
519         drawn_menu = nil
520
521         screen.defcursor()
522         print("Exiting menu!")
523 end
524
525 function menu.autoboot(delay)
526         local x = loader.getenv("loader_menu_timeout_x") or 4
527         local y = loader.getenv("loader_menu_timeout_y") or 23
528         local endtime = loader.time() + delay
529         local time
530         local last
531         repeat
532                 time = endtime - loader.time()
533                 if last == nil or last ~= time then
534                         last = time
535                         screen.setcursor(x, y)
536                         print("Autoboot in " .. time ..
537                             " seconds. [Space] to pause ")
538                         screen.defcursor()
539                 end
540                 if io.ischar() then
541                         local ch = io.getchar()
542                         if ch == core.KEY_ENTER then
543                                 break
544                         else
545                                 -- erase autoboot msg
546                                 screen.setcursor(0, y)
547                                 print(string.rep(" ", 80))
548                                 screen.defcursor()
549                                 return ch
550                         end
551                 end
552
553                 loader.delay(50000)
554         until time <= 0
555
556         local cmd = loader.getenv("menu_timeout_command") or "boot"
557         cli_execute_unparsed(cmd)
558         return nil
559 end
560
561 -- CLI commands
562 function cli.menu()
563         menu.run()
564 end
565
566 return menu