xref: /freebsd-12.1/stand/lua/menu.lua (revision ca16d83f)
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 screen_invalid = true
42
43local function OnOff(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 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		screen_invalid = true
86		menu.process(entry.submenu)
87	end,
88	[core.MENU_RETURN] = function(_, 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(_, choice, _)
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.deepCopyTable(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(_, choice, _)
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-- current_alias_table will be used to keep our alias table consistent across
347-- screen redraws, instead of relying on whatever triggered the redraw to update
348-- the local alias_table in menu.process.
349menu.current_alias_table = {}
350
351function menu.redraw(m)
352	-- redraw screen
353	screen.clear()
354	screen.defcursor()
355	menu.current_alias_table = drawer.drawscreen(m)
356	screen_invalid = false
357end
358
359-- 'keypress' allows the caller to indicate that a key has been pressed that we
360-- should process as our initial input.
361function menu.process(m, keypress)
362	assert(m ~= nil)
363
364	-- Trigger a redraw if we've been invalidated.  Otherwise, we assume
365	-- that this menu has already been drawn.
366	if screen_invalid then
367		menu.redraw(m)
368	end
369
370	while true do
371		local key = keypress or io.getchar()
372		keypress = nil
373
374		-- Special key behaviors
375		if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and
376		    m ~= menu.default then
377			break
378		elseif key == core.KEY_ENTER then
379			core.boot()
380			-- Should not return
381		end
382
383		key = string.char(key)
384		-- check to see if key is an alias
385		local sel_entry = nil
386		for k, v in pairs(menu.current_alias_table) do
387			if key == k then
388				sel_entry = v
389				break
390			end
391		end
392
393		-- if we have an alias do the assigned action:
394		if sel_entry ~= nil then
395			-- Get menu handler
396			local handler = menu.handlers[sel_entry.entry_type]
397			if handler ~= nil then
398				-- The handler's return value indicates if we
399				-- need to exit this menu.  An omitted or true
400				-- return value means to continue.
401				if handler(m, sel_entry) == false then
402					return
403				end
404			end
405			-- If we got an alias key the screen is out of date...
406			-- redraw it.
407			menu.redraw(m)
408		end
409	end
410end
411
412function menu.run()
413	if menu.skip() then
414		core.autoboot()
415		return
416	end
417
418	menu.redraw(menu.default)
419	local autoboot_key = menu.autoboot()
420
421	menu.process(menu.default, autoboot_key)
422
423	screen.defcursor()
424	print("Exiting menu!")
425end
426
427function menu.skip()
428	if core.isSerialBoot() then
429		return true
430	end
431	local c = string.lower(loader.getenv("console") or "")
432	if c:match("^efi[ ;]") ~= nil or c:match("[ ;]efi[ ;]") ~= nil then
433		return true
434	end
435
436	c = string.lower(loader.getenv("beastie_disable") or "")
437	print("beastie_disable", c)
438	return c == "yes"
439end
440
441function menu.autoboot()
442	local ab = loader.getenv("autoboot_delay")
443	if ab ~= nil and ab:lower() == "no" then
444		return nil
445	elseif tonumber(ab) == -1 then
446		core.boot()
447	end
448	ab = tonumber(ab) or 10
449
450	local x = loader.getenv("loader_menu_timeout_x") or 5
451	local y = loader.getenv("loader_menu_timeout_y") or 22
452
453	local endtime = loader.time() + ab
454	local time
455
456	repeat
457		time = endtime - loader.time()
458		screen.setcursor(x, y)
459		print("Autoboot in " .. time ..
460		    " seconds, hit [Enter] to boot" ..
461		    " or any other key to stop     ")
462		screen.defcursor()
463		if io.ischar() then
464			local ch = io.getchar()
465			if ch == core.KEY_ENTER then
466				break
467			else
468				-- erase autoboot msg
469				screen.setcursor(0, y)
470				print(string.rep(" ", 80))
471				screen.defcursor()
472				return ch
473			end
474		end
475
476		loader.delay(50000)
477	until time <= 0
478	core.boot()
479
480end
481
482return menu
483