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