xref: /freebsd-14.2/stand/lua/menu.lua (revision 3049d4cc)
1--
2-- SPDX-License-Identifier: BSD-2-Clause-FreeBSD
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-- $FreeBSD$
30--
31
32
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
42
43local function OnOff(str, value)
44	if value 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 function bootenvSet(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(_, 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		menu.process(entry.submenu)
86	end,
87	[core.MENU_RETURN] = function(_, entry)
88		-- allow entry to have a function/side effect
89		if entry.func ~= nil then
90			entry.func()
91		end
92		return false
93	end,
94}
95-- loader menu tree is rooted at menu.welcome
96
97menu.boot_environments = {
98	entries = {
99		-- return to welcome menu
100		{
101			entry_type = core.MENU_RETURN,
102			name = "Back to main menu" ..
103			    color.highlight(" [Backspace]"),
104		},
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.escapef(color.GREEN)
119				else
120					name_color = color.escapef(color.BLUE)
121				end
122				bootenv_name = bootenv_name .. name_color ..
123				    choice .. color.default()
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		{
153			entry_type = core.MENU_RETURN,
154			name = "Back to main menu" ..
155			    color.highlight(" [Backspace]"),
156		},
157		-- load defaults
158		{
159			entry_type = core.MENU_ENTRY,
160			name = "Load System " .. color.highlight("D") ..
161			    "efaults",
162			func = core.setDefaults,
163			alias = {"d", "D"},
164		},
165		{
166			entry_type = core.MENU_SEPARATOR,
167		},
168		{
169			entry_type = core.MENU_SEPARATOR,
170			name = "Boot Options:",
171		},
172		-- acpi
173		{
174			entry_type = core.MENU_ENTRY,
175			visible = core.isSystem386,
176			name = function()
177				return OnOff(color.highlight("A") ..
178				    "CPI       :", core.acpi)
179			end,
180			func = core.setACPI,
181			alias = {"a", "A"},
182		},
183		-- safe mode
184		{
185			entry_type = core.MENU_ENTRY,
186			name = function()
187				return OnOff("Safe " .. color.highlight("M") ..
188				    "ode  :", core.sm)
189			end,
190			func = core.setSafeMode,
191			alias = {"m", "M"},
192		},
193		-- single user
194		{
195			entry_type = core.MENU_ENTRY,
196			name = function()
197				return OnOff(color.highlight("S") ..
198				    "ingle user:", core.su)
199			end,
200			func = core.setSingleUser,
201			alias = {"s", "S"},
202		},
203		-- verbose boot
204		{
205			entry_type = core.MENU_ENTRY,
206			name = function()
207				return OnOff(color.highlight("V") ..
208				    "erbose    :", core.verbose)
209			end,
210			func = core.setVerbose,
211			alias = {"v", "V"},
212		},
213	},
214}
215
216menu.welcome = {
217	entries = function()
218		local menu_entries = menu.welcome.all_entries
219		-- Swap the first two menu items on single user boot
220		if core.isSingleUserBoot() then
221			-- We'll cache the swapped menu, for performance
222			if menu.welcome.swapped_menu ~= nil then
223				return menu.welcome.swapped_menu
224			end
225			-- Shallow copy the table
226			menu_entries = core.deepCopyTable(menu_entries)
227
228			-- Swap the first two menu entries
229			menu_entries[1], menu_entries[2] =
230			    menu_entries[2], menu_entries[1]
231
232			-- Then set their names to their alternate names
233			menu_entries[1].name, menu_entries[2].name =
234			    menu_entries[1].alternate_name,
235			    menu_entries[2].alternate_name
236			menu.welcome.swapped_menu = menu_entries
237		end
238		return menu_entries
239	end,
240	all_entries = {
241		-- boot multi user
242		{
243			entry_type = core.MENU_ENTRY,
244			name = color.highlight("B") .. "oot Multi user " ..
245			    color.highlight("[Enter]"),
246			-- Not a standard menu entry function!
247			alternate_name = color.highlight("B") ..
248			    "oot Multi user",
249			func = function()
250				core.setSingleUser(false)
251				core.boot()
252			end,
253			alias = {"b", "B"},
254		},
255		-- boot single user
256		{
257			entry_type = core.MENU_ENTRY,
258			name = "Boot " .. color.highlight("S") .. "ingle user",
259			-- Not a standard menu entry function!
260			alternate_name = "Boot " .. color.highlight("S") ..
261			    "ingle user " .. color.highlight("[Enter]"),
262			func = function()
263				core.setSingleUser(true)
264				core.boot()
265			end,
266			alias = {"s", "S"},
267		},
268		-- escape to interpreter
269		{
270			entry_type = core.MENU_RETURN,
271			name = color.highlight("Esc") .. "ape to loader prompt",
272			func = function()
273				loader.setenv("autoboot_delay", "NO")
274			end,
275			alias = {core.KEYSTR_ESCAPE},
276		},
277		-- reboot
278		{
279			entry_type = core.MENU_ENTRY,
280			name = color.highlight("R") .. "eboot",
281			func = function()
282				loader.perform("reboot")
283			end,
284			alias = {"r", "R"},
285		},
286		{
287			entry_type = core.MENU_SEPARATOR,
288		},
289		{
290			entry_type = core.MENU_SEPARATOR,
291			name = "Options:",
292		},
293		-- kernel options
294		{
295			entry_type = core.MENU_CAROUSEL_ENTRY,
296			carousel_id = "kernel",
297			items = core.kernelList,
298			name = function(idx, choice, all_choices)
299				if #all_choices == 0 then
300					return "Kernel: "
301				end
302
303				local is_default = (idx == 1)
304				local kernel_name = ""
305				local name_color
306				if is_default then
307					name_color = color.escapef(color.GREEN)
308					kernel_name = "default/"
309				else
310					name_color = color.escapef(color.BLUE)
311				end
312				kernel_name = kernel_name .. name_color ..
313				    choice .. color.default()
314				return color.highlight("K") .. "ernel: " ..
315				    kernel_name .. " (" .. idx .. " of " ..
316				    #all_choices .. ")"
317			end,
318			func = function(_, choice, _)
319				config.selectKernel(choice)
320			end,
321			alias = {"k", "K"},
322		},
323		-- boot options
324		{
325			entry_type = core.MENU_SUBMENU,
326			name = "Boot " .. color.highlight("O") .. "ptions",
327			submenu = menu.boot_options,
328			alias = {"o", "O"},
329		},
330		-- boot environments
331		{
332			entry_type = core.MENU_SUBMENU,
333			visible = function()
334				return core.isZFSBoot() and
335				    #core.bootenvList() > 1
336			end,
337			name = "Boot " .. color.highlight("E") .. "nvironments",
338			submenu = menu.boot_environments,
339			alias = {"e", "E"},
340		},
341	},
342}
343
344menu.default = menu.welcome
345-- current_alias_table will be used to keep our alias table consistent across
346-- screen redraws, instead of relying on whatever triggered the redraw to update
347-- the local alias_table in menu.process.
348menu.current_alias_table = {}
349
350function menu.draw(menudef)
351	-- Clear the screen, reset the cursor, then draw
352	screen.clear()
353	screen.defcursor()
354	menu.current_alias_table = drawer.drawscreen(menudef)
355	drawn_menu = menudef
356end
357
358-- 'keypress' allows the caller to indicate that a key has been pressed that we
359-- should process as our initial input.
360function menu.process(menudef, keypress)
361	assert(menudef ~= nil)
362
363	if drawn_menu ~= menudef then
364		menu.draw(menudef)
365	end
366
367	while true do
368		local key = keypress or io.getchar()
369		keypress = nil
370
371		-- Special key behaviors
372		if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and
373		    menudef ~= menu.default then
374			break
375		elseif key == core.KEY_ENTER then
376			core.boot()
377			-- Should not return
378		end
379
380		key = string.char(key)
381		-- check to see if key is an alias
382		local sel_entry = nil
383		for k, v in pairs(menu.current_alias_table) do
384			if key == k then
385				sel_entry = v
386				break
387			end
388		end
389
390		-- if we have an alias do the assigned action:
391		if sel_entry ~= nil then
392			local handler = menu.handlers[sel_entry.entry_type]
393			assert(handler ~= nil)
394			-- The handler's return value indicates if we
395			-- need to exit this menu.  An omitted or true
396			-- return value means to continue.
397			if handler(menudef, sel_entry) == false then
398				return
399			end
400			-- If we got an alias key the screen is out of date...
401			-- redraw it.
402			menu.draw(menudef)
403		end
404	end
405end
406
407function menu.run()
408	menu.draw(menu.default)
409	local autoboot_key = menu.autoboot()
410
411	menu.process(menu.default, autoboot_key)
412	drawn_menu = nil
413
414	screen.defcursor()
415	print("Exiting menu!")
416end
417
418function menu.autoboot()
419	local ab = loader.getenv("autoboot_delay")
420	if ab ~= nil and ab:lower() == "no" then
421		return nil
422	elseif tonumber(ab) == -1 then
423		core.boot()
424	end
425	ab = tonumber(ab) or 10
426
427	local x = loader.getenv("loader_menu_timeout_x") or 5
428	local y = loader.getenv("loader_menu_timeout_y") or 22
429
430	local endtime = loader.time() + ab
431	local time
432
433	repeat
434		time = endtime - loader.time()
435		screen.setcursor(x, y)
436		print("Autoboot in " .. time ..
437		    " seconds, hit [Enter] to boot" ..
438		    " or any other key to stop     ")
439		screen.defcursor()
440		if io.ischar() then
441			local ch = io.getchar()
442			if ch == core.KEY_ENTER then
443				break
444			else
445				-- erase autoboot msg
446				screen.setcursor(0, y)
447				print(string.rep(" ", 80))
448				screen.defcursor()
449				return ch
450			end
451		end
452
453		loader.delay(50000)
454	until time <= 0
455	core.boot()
456
457end
458
459return menu
460