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