menu.lua revision 360598
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: stable/11/stand/lua/menu.lua 360598 2020-05-03 03:56:17Z kevans $
30--
31
32local cli = require("cli")
33local core = require("core")
34local color = require("color")
35local config = require("config")
36local screen = require("screen")
37local drawer = require("drawer")
38
39local menu = {}
40
41local drawn_menu
42local return_menu_entry = {
43	entry_type = core.MENU_RETURN,
44	name = "Back to main menu" .. color.highlight(" [Backspace]"),
45}
46
47local 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
55end
56
57local function bootenvSet(env)
58	loader.setenv("vfs.root.mountfrom", env)
59	loader.setenv("currdev", env .. ":")
60	config.reload()
61end
62
63-- Module exports
64menu.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
101menu.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
149menu.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
212menu.welcome = {
213	entries = function()
214		local menu_entries = menu.welcome.all_entries
215		local multi_user = menu_entries.multi_user
216		local single_user = menu_entries.single_user
217		local boot_entry_1, boot_entry_2
218		if core.isSingleUserBoot() then
219			-- Swap the first two menu items on single user boot.
220			-- We'll cache the alternate entries for performance.
221			local alts = menu_entries.alts
222			if alts == nil then
223				single_user = core.deepCopyTable(single_user)
224				multi_user = core.deepCopyTable(multi_user)
225				single_user.name = single_user.alternate_name
226				multi_user.name = multi_user.alternate_name
227				menu_entries.alts = {
228					single_user = single_user,
229					multi_user = multi_user,
230				}
231			else
232				single_user = alts.single_user 
233				multi_user = alts.multi_user
234			end
235			boot_entry_1, boot_entry_2 = single_user, multi_user
236		else
237			boot_entry_1, boot_entry_2 = multi_user, single_user
238		end
239		return {
240			boot_entry_1,
241			boot_entry_2,
242			menu_entries.prompt,
243			menu_entries.reboot,
244			{
245				entry_type = core.MENU_SEPARATOR,
246			},
247			{
248				entry_type = core.MENU_SEPARATOR,
249				name = "Options:",
250			},
251			menu_entries.kernel_options,
252			menu_entries.boot_options,
253			menu_entries.boot_envs,
254			menu_entries.chainload,
255		}
256	end,
257	all_entries = {
258		multi_user = {
259			entry_type = core.MENU_ENTRY,
260			name = color.highlight("B") .. "oot Multi user " ..
261			    color.highlight("[Enter]"),
262			-- Not a standard menu entry function!
263			alternate_name = color.highlight("B") ..
264			    "oot Multi user",
265			func = function()
266				core.setSingleUser(false)
267				core.boot()
268			end,
269			alias = {"b", "B"},
270		},
271		single_user = {
272			entry_type = core.MENU_ENTRY,
273			name = "Boot " .. color.highlight("S") .. "ingle user",
274			-- Not a standard menu entry function!
275			alternate_name = "Boot " .. color.highlight("S") ..
276			    "ingle user " .. color.highlight("[Enter]"),
277			func = function()
278				core.setSingleUser(true)
279				core.boot()
280			end,
281			alias = {"s", "S"},
282		},
283		prompt = {
284			entry_type = core.MENU_RETURN,
285			name = color.highlight("Esc") .. "ape to loader prompt",
286			func = function()
287				loader.setenv("autoboot_delay", "NO")
288			end,
289			alias = {core.KEYSTR_ESCAPE},
290		},
291		reboot = {
292			entry_type = core.MENU_ENTRY,
293			name = color.highlight("R") .. "eboot",
294			func = function()
295				loader.perform("reboot")
296			end,
297			alias = {"r", "R"},
298		},
299		kernel_options = {
300			entry_type = core.MENU_CAROUSEL_ENTRY,
301			carousel_id = "kernel",
302			items = core.kernelList,
303			name = function(idx, choice, all_choices)
304				if #all_choices == 0 then
305					return "Kernel: "
306				end
307
308				local is_default = (idx == 1)
309				local kernel_name = ""
310				local name_color
311				if is_default then
312					name_color = color.escapefg(color.GREEN)
313					kernel_name = "default/"
314				else
315					name_color = color.escapefg(color.BLUE)
316				end
317				kernel_name = kernel_name .. name_color ..
318				    choice .. color.resetfg()
319				return color.highlight("K") .. "ernel: " ..
320				    kernel_name .. " (" .. idx .. " of " ..
321				    #all_choices .. ")"
322			end,
323			func = function(_, choice, _)
324				if loader.getenv("kernelname") ~= nil then
325					loader.perform("unload")
326				end
327				config.selectKernel(choice)
328			end,
329			alias = {"k", "K"},
330		},
331		boot_options = {
332			entry_type = core.MENU_SUBMENU,
333			name = "Boot " .. color.highlight("O") .. "ptions",
334			submenu = menu.boot_options,
335			alias = {"o", "O"},
336		},
337		boot_envs = {
338			entry_type = core.MENU_SUBMENU,
339			visible = function()
340				return core.isZFSBoot() and
341				    #core.bootenvList() > 1
342			end,
343			name = "Boot " .. color.highlight("E") .. "nvironments",
344			submenu = menu.boot_environments,
345			alias = {"e", "E"},
346		},
347		chainload = {
348			entry_type = core.MENU_ENTRY,
349			name = function()
350				return 'Chain' .. color.highlight("L") ..
351				    "oad " .. loader.getenv('chain_disk')
352			end,
353			func = function()
354				loader.perform("chain " ..
355				    loader.getenv('chain_disk'))
356			end,
357			visible = function()
358				return loader.getenv('chain_disk') ~= nil
359			end,
360			alias = {"l", "L"},
361		},
362	},
363}
364
365menu.default = menu.welcome
366-- current_alias_table will be used to keep our alias table consistent across
367-- screen redraws, instead of relying on whatever triggered the redraw to update
368-- the local alias_table in menu.process.
369menu.current_alias_table = {}
370
371function menu.draw(menudef)
372	-- Clear the screen, reset the cursor, then draw
373	screen.clear()
374	menu.current_alias_table = drawer.drawscreen(menudef)
375	drawn_menu = menudef
376	screen.defcursor()
377end
378
379-- 'keypress' allows the caller to indicate that a key has been pressed that we
380-- should process as our initial input.
381function menu.process(menudef, keypress)
382	assert(menudef ~= nil)
383
384	if drawn_menu ~= menudef then
385		menu.draw(menudef)
386	end
387
388	while true do
389		local key = keypress or io.getchar()
390		keypress = nil
391
392		-- Special key behaviors
393		if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and
394		    menudef ~= menu.default then
395			break
396		elseif key == core.KEY_ENTER then
397			core.boot()
398			-- Should not return.  If it does, escape menu handling
399			-- and drop to loader prompt.
400			return false
401		end
402
403		key = string.char(key)
404		-- check to see if key is an alias
405		local sel_entry = nil
406		for k, v in pairs(menu.current_alias_table) do
407			if key == k then
408				sel_entry = v
409				break
410			end
411		end
412
413		-- if we have an alias do the assigned action:
414		if sel_entry ~= nil then
415			local handler = menu.handlers[sel_entry.entry_type]
416			assert(handler ~= nil)
417			-- The handler's return value indicates if we
418			-- need to exit this menu.  An omitted or true
419			-- return value means to continue.
420			if handler(menudef, sel_entry) == false then
421				return
422			end
423			-- If we got an alias key the screen is out of date...
424			-- redraw it.
425			menu.draw(menudef)
426		end
427	end
428end
429
430function menu.run()
431	local autoboot_key
432	local delay = loader.getenv("autoboot_delay")
433
434	if delay ~= nil and delay:lower() == "no" then
435		delay = nil
436	else
437		delay = tonumber(delay) or 10
438	end
439
440	if delay == -1 then
441		core.boot()
442		return
443	end
444
445	menu.draw(menu.default)
446
447	if delay ~= nil then
448		autoboot_key = menu.autoboot(delay)
449
450		-- autoboot_key should return the key pressed.  It will only
451		-- return nil if we hit the timeout and executed the timeout
452		-- command.  Bail out.
453		if autoboot_key == nil then
454			return
455		end
456	end
457
458	menu.process(menu.default, autoboot_key)
459	drawn_menu = nil
460
461	screen.defcursor()
462	print("Exiting menu!")
463end
464
465function menu.autoboot(delay)
466	local x = loader.getenv("loader_menu_timeout_x") or 4
467	local y = loader.getenv("loader_menu_timeout_y") or 23
468	local endtime = loader.time() + delay
469	local time
470	local last
471	repeat
472		time = endtime - loader.time()
473		if last == nil or last ~= time then
474			last = time
475			screen.setcursor(x, y)
476			print("Autoboot in " .. time ..
477			    " seconds, hit [Enter] to boot" ..
478			    " or any other key to stop     ")
479			screen.defcursor()
480		end
481		if io.ischar() then
482			local ch = io.getchar()
483			if ch == core.KEY_ENTER then
484				break
485			else
486				-- erase autoboot msg
487				screen.setcursor(0, y)
488				print(string.rep(" ", 80))
489				screen.defcursor()
490				return ch
491			end
492		end
493
494		loader.delay(50000)
495	until time <= 0
496
497	local cmd = loader.getenv("menu_timeout_command") or "boot"
498	cli_execute_unparsed(cmd)
499	return nil
500end
501
502-- CLI commands
503function cli.menu()
504	menu.run()
505end
506
507return menu
508