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