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