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