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