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