xref: /freebsd-13.1/stand/lua/menu.lua (revision 4f866698)
1--
2-- Copyright (c) 2015 Pedro Souza <[email protected]>
3-- Copyright (C) 2018 Kyle Evans <[email protected]>
4-- All rights reserved.
5--
6-- Redistribution and use in source and binary forms, with or without
7-- modification, are permitted provided that the following conditions
8-- are met:
9-- 1. Redistributions of source code must retain the above copyright
10--    notice, this list of conditions and the following disclaimer.
11-- 2. Redistributions in binary form must reproduce the above copyright
12--    notice, this list of conditions and the following disclaimer in the
13--    documentation and/or other materials provided with the distribution.
14--
15-- THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18-- ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23-- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24-- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25-- SUCH DAMAGE.
26--
27-- $FreeBSD$
28--
29
30
31local core = require("core")
32local color = require("color")
33local config = require("config")
34local screen = require("screen")
35local drawer = require("drawer")
36
37local menu = {}
38
39local skip
40local run
41local autoboot
42
43local OnOff = function(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
51end
52
53local bootenvSet = function(env)
54	loader.setenv("vfs.root.mountfrom", env)
55	loader.setenv("currdev", env .. ":")
56	config.reload()
57end
58
59-- Module exports
60menu.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(current_menu, entry)
67		-- run function
68		entry.func()
69	end,
70	[core.MENU_CAROUSEL_ENTRY] = function(current_menu, 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(current_menu, entry)
85		-- recurse
86		return menu.run(entry.submenu)
87	end,
88	[core.MENU_RETURN] = function(current_menu, 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
98menu.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(idx, choice, all_choices)
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
150menu.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
217menu.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.shallowCopyTable(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(idx, choice, all_choices)
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
345menu.default = menu.welcome
346
347function menu.run(m)
348
349	if menu.skip() then
350		core.autoboot()
351		return false
352	end
353
354	if m == nil then
355		m = menu.default
356	end
357
358	-- redraw screen
359	screen.clear()
360	screen.defcursor()
361	local alias_table = drawer.drawscreen(m)
362
363	-- Might return nil, that's ok
364	local autoboot_key;
365	if m == menu.default then
366		autoboot_key = menu.autoboot()
367	end
368	cont = true
369	while cont do
370		local key = autoboot_key or io.getchar()
371		autoboot_key = nil
372
373		-- Special key behaviors
374		if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and
375		    m ~= menu.default then
376			break
377		elseif key == core.KEY_ENTER then
378			core.boot()
379			-- Should not return
380		end
381
382		key = string.char(key)
383		-- check to see if key is an alias
384		local sel_entry = nil
385		for k, v in pairs(alias_table) do
386			if key == k then
387				sel_entry = v
388			end
389		end
390
391		-- if we have an alias do the assigned action:
392		if sel_entry ~= nil then
393			-- Get menu handler
394			local handler = menu.handlers[sel_entry.entry_type]
395			if handler ~= nil then
396				-- The handler's return value indicates whether
397				-- we need to exit this menu. An omitted return
398				-- value means "continue" by default.
399				cont = handler(m, sel_entry)
400				if cont == nil then
401					cont = true
402				end
403			end
404			-- if we got an alias key the screen is out of date:
405			screen.clear()
406			screen.defcursor()
407			alias_table = drawer.drawscreen(m)
408		end
409	end
410
411	if m == menu.default then
412		screen.defcursor()
413		print("Exiting menu!")
414		return false
415	end
416
417	return true
418end
419
420function menu.skip()
421	if core.isSerialBoot() then
422		return true
423	end
424	local c = string.lower(loader.getenv("console") or "")
425	if c:match("^efi[ ;]") ~= nil or c:match("[ ;]efi[ ;]") ~= nil then
426		return true
427	end
428
429	c = string.lower(loader.getenv("beastie_disable") or "")
430	print("beastie_disable", c)
431	return c == "yes"
432end
433
434function menu.autoboot()
435	local ab = loader.getenv("autoboot_delay")
436	if ab ~= nil and ab:lower() == "no" then
437		return nil
438	elseif tonumber(ab) == -1 then
439		core.boot()
440	end
441	ab = tonumber(ab) or 10
442
443	local x = loader.getenv("loader_menu_timeout_x") or 5
444	local y = loader.getenv("loader_menu_timeout_y") or 22
445
446	local endtime = loader.time() + ab
447	local time
448
449	repeat
450		time = endtime - loader.time()
451		screen.setcursor(x, y)
452		print("Autoboot in " .. time ..
453		    " seconds, hit [Enter] to boot" ..
454		    " or any other key to stop     ")
455		screen.defcursor()
456		if io.ischar() then
457			local ch = io.getchar()
458			if ch == core.KEY_ENTER then
459				break
460			else
461				-- erase autoboot msg
462				screen.setcursor(0, y)
463				print("                                        "
464				    .. "                                        ")
465				screen.defcursor()
466				return ch
467			end
468		end
469
470		loader.delay(50000)
471	until time <= 0
472	core.boot()
473
474end
475
476return menu
477