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