xref: /freebsd/stand/lua/menu.lua (revision f0cb3b6b258df56324123c8732a9b5470a88383c)
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[ ;]") 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