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