xref: /freebsd/stand/lua/drawer.lua (revision 3d265fce43746c293ae826e9603adbfe09f93cf6)
1--
2-- SPDX-License-Identifier: BSD-2-Clause-FreeBSD
3--
4-- Copyright (c) 2015 Pedro Souza <pedrosouza@freebsd.org>
5-- Copyright (c) 2018 Kyle Evans <kevans@FreeBSD.org>
6-- All rights reserved.
7--
8-- Redistribution and use in source and binary forms, with or without
9-- modification, are permitted provided that the following conditions
10-- are met:
11-- 1. Redistributions of source code must retain the above copyright
12--    notice, this list of conditions and the following disclaimer.
13-- 2. Redistributions in binary form must reproduce the above copyright
14--    notice, this list of conditions and the following disclaimer in the
15--    documentation and/or other materials provided with the distribution.
16--
17-- THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
18-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20-- ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
21-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
23-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25-- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
26-- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
27-- SUCH DAMAGE.
28--
29-- $FreeBSD$
30--
31
32local color = require("color")
33local config = require("config")
34local core = require("core")
35local screen = require("screen")
36
37local drawer = {}
38
39local fbsd_brand
40local none
41
42local menu_name_handlers
43local branddefs
44local logodefs
45local brand_position
46local logo_position
47local menu_position
48local frame_size
49local default_shift
50local shift
51
52local function menuEntryName(drawing_menu, entry)
53	local name_handler = menu_name_handlers[entry.entry_type]
54
55	if name_handler ~= nil then
56		return name_handler(drawing_menu, entry)
57	end
58	if type(entry.name) == "function" then
59		return entry.name()
60	end
61	return entry.name
62end
63
64local function processFile(gfxname)
65	if gfxname == nil then
66		return false, "Missing filename"
67	end
68
69	local ret = try_include('gfx-' .. gfxname)
70	if ret == nil then
71		return false, "Failed to include gfx-" .. gfxname
72	end
73
74	-- Legacy format
75	if type(ret) ~= "table" then
76		return true
77	end
78
79	for gfxtype, def in pairs(ret) do
80		if gfxtype == "brand" then
81			drawer.addBrand(gfxname, def)
82		elseif gfxtype == "logo" then
83			drawer.addLogo(gfxname, def)
84		else
85			return false, "Unknown graphics type '" .. gfxtype ..
86			    "'"
87		end
88	end
89
90	return true
91end
92
93local function getBranddef(brand)
94	if brand == nil then
95		return nil
96	end
97	-- Look it up
98	local branddef = branddefs[brand]
99
100	-- Try to pull it in
101	if branddef == nil then
102		local res, err = processFile(brand)
103		if not res then
104			-- This fallback should go away after FreeBSD 13.
105			try_include('brand-' .. brand)
106			-- If the fallback also failed, print whatever error
107			-- we encountered in the original processing.
108			if branddefs[brand] == nil then
109				print(err)
110				return nil
111			end
112		end
113
114		branddef = branddefs[brand]
115	end
116
117	return branddef
118end
119
120local function getLogodef(logo)
121	if logo == nil then
122		return nil
123	end
124	-- Look it up
125	local logodef = logodefs[logo]
126
127	-- Try to pull it in
128	if logodef == nil then
129		local res, err = processFile(logo)
130		if not res then
131			-- This fallback should go away after FreeBSD 13.
132			try_include('logo-' .. logo)
133			-- If the fallback also failed, print whatever error
134			-- we encountered in the original processing.
135			if logodefs[logo] == nil then
136				print(err)
137				return nil
138			end
139		end
140
141		logodef = logodefs[logo]
142	end
143
144	return logodef
145end
146
147local function draw(x, y, logo)
148	for i = 1, #logo do
149		screen.setcursor(x, y + i - 1)
150		printc(logo[i])
151	end
152end
153
154local function drawmenu(menudef)
155	local x = menu_position.x
156	local y = menu_position.y
157
158	x = x + shift.x
159	y = y + shift.y
160
161	-- print the menu and build the alias table
162	local alias_table = {}
163	local entry_num = 0
164	local menu_entries = menudef.entries
165	local effective_line_num = 0
166	if type(menu_entries) == "function" then
167		menu_entries = menu_entries()
168	end
169	for _, e in ipairs(menu_entries) do
170		-- Allow menu items to be conditionally visible by specifying
171		-- a visible function.
172		if e.visible ~= nil and not e.visible() then
173			goto continue
174		end
175		effective_line_num = effective_line_num + 1
176		if e.entry_type ~= core.MENU_SEPARATOR then
177			entry_num = entry_num + 1
178			screen.setcursor(x, y + effective_line_num)
179
180			printc(entry_num .. ". " .. menuEntryName(menudef, e))
181
182			-- fill the alias table
183			alias_table[tostring(entry_num)] = e
184			if e.alias ~= nil then
185				for _, a in ipairs(e.alias) do
186					alias_table[a] = e
187				end
188			end
189		else
190			screen.setcursor(x, y + effective_line_num)
191			printc(menuEntryName(menudef, e))
192		end
193		::continue::
194	end
195	return alias_table
196end
197
198local function defaultframe()
199	if core.isSerialConsole() then
200		return "ascii"
201	end
202	return "double"
203end
204
205local function drawbox()
206	local x = menu_position.x - 3
207	local y = menu_position.y - 1
208	local w = frame_size.w
209	local h = frame_size.h
210
211	local framestyle = loader.getenv("loader_menu_frame") or defaultframe()
212	local framespec = drawer.frame_styles[framestyle]
213	-- If we don't have a framespec for the current frame style, just don't
214	-- draw a box.
215	if framespec == nil then
216		return
217	end
218
219	local hl = framespec.horizontal
220	local vl = framespec.vertical
221
222	local tl = framespec.top_left
223	local bl = framespec.bottom_left
224	local tr = framespec.top_right
225	local br = framespec.bottom_right
226
227	x = x + shift.x
228	y = y + shift.y
229
230	screen.setcursor(x, y); printc(tl)
231	screen.setcursor(x, y + h); printc(bl)
232	screen.setcursor(x + w, y); printc(tr)
233	screen.setcursor(x + w, y + h); printc(br)
234
235	screen.setcursor(x + 1, y)
236	for _ = 1, w - 1 do
237		printc(hl)
238	end
239
240	screen.setcursor(x + 1, y + h)
241	for _ = 1, w - 1 do
242		printc(hl)
243	end
244
245	for i = 1, h - 1 do
246		screen.setcursor(x, y + i)
247		printc(vl)
248		screen.setcursor(x + w, y + i)
249		printc(vl)
250	end
251
252	local menu_header = loader.getenv("loader_menu_title") or
253	    "Welcome to FreeBSD"
254	local menu_header_align = loader.getenv("loader_menu_title_align")
255	local menu_header_x
256
257	if menu_header_align ~= nil then
258		menu_header_align = menu_header_align:lower()
259		if menu_header_align == "left" then
260			-- Just inside the left border on top
261			menu_header_x = x + 1
262		elseif menu_header_align == "right" then
263			-- Just inside the right border on top
264			menu_header_x = x + w - #menu_header
265		end
266	end
267	if menu_header_x == nil then
268		menu_header_x = x + (w / 2) - (#menu_header / 2)
269	end
270	screen.setcursor(menu_header_x, y)
271	printc(menu_header)
272end
273
274local function drawbrand()
275	local x = tonumber(loader.getenv("loader_brand_x")) or
276	    brand_position.x
277	local y = tonumber(loader.getenv("loader_brand_y")) or
278	    brand_position.y
279
280	local branddef = getBranddef(loader.getenv("loader_brand"))
281
282	if branddef == nil then
283		branddef = getBranddef(drawer.default_brand)
284	end
285
286	local graphic = branddef.graphic
287
288	x = x + shift.x
289	y = y + shift.y
290	draw(x, y, graphic)
291end
292
293local function drawlogo()
294	local x = tonumber(loader.getenv("loader_logo_x")) or
295	    logo_position.x
296	local y = tonumber(loader.getenv("loader_logo_y")) or
297	    logo_position.y
298
299	local logo = loader.getenv("loader_logo")
300	local colored = color.isEnabled()
301
302	local logodef = getLogodef(logo)
303
304	if logodef == nil or logodef.graphic == nil or
305	    (not colored and logodef.requires_color) then
306		-- Choose a sensible default
307		if colored then
308			logodef = getLogodef(drawer.default_color_logodef)
309		else
310			logodef = getLogodef(drawer.default_bw_logodef)
311		end
312
313		-- Something has gone terribly wrong.
314		if logodef == nil then
315			logodef = getLogodef(drawer.default_fallback_logodef)
316		end
317	end
318
319	if logodef ~= nil and logodef.graphic == none then
320		shift = logodef.shift
321	else
322		shift = default_shift
323	end
324
325	x = x + shift.x
326	y = y + shift.y
327
328	if logodef ~= nil and logodef.shift ~= nil then
329		x = x + logodef.shift.x
330		y = y + logodef.shift.y
331	end
332
333	draw(x, y, logodef.graphic)
334end
335
336fbsd_brand = {
337"  ______               ____   _____ _____  ",
338" |  ____|             |  _ \\ / ____|  __ \\ ",
339" | |___ _ __ ___  ___ | |_) | (___ | |  | |",
340" |  ___| '__/ _ \\/ _ \\|  _ < \\___ \\| |  | |",
341" | |   | | |  __/  __/| |_) |____) | |__| |",
342" | |   | | |    |    ||     |      |      |",
343" |_|   |_|  \\___|\\___||____/|_____/|_____/ "
344}
345none = {""}
346
347menu_name_handlers = {
348	-- Menu name handlers should take the menu being drawn and entry being
349	-- drawn as parameters, and return the name of the item.
350	-- This is designed so that everything, including menu separators, may
351	-- have their names derived differently. The default action for entry
352	-- types not specified here is to use entry.name directly.
353	[core.MENU_SEPARATOR] = function(_, entry)
354		if entry.name ~= nil then
355			if type(entry.name) == "function" then
356				return entry.name()
357			end
358			return entry.name
359		end
360		return ""
361	end,
362	[core.MENU_CAROUSEL_ENTRY] = function(_, entry)
363		local carid = entry.carousel_id
364		local caridx = config.getCarouselIndex(carid)
365		local choices = entry.items
366		if type(choices) == "function" then
367			choices = choices()
368		end
369		if #choices < caridx then
370			caridx = 1
371		end
372		return entry.name(caridx, choices[caridx], choices)
373	end,
374}
375
376branddefs = {
377	-- Indexed by valid values for loader_brand in loader.conf(5). Valid
378	-- keys are: graphic (table depicting graphic)
379	["fbsd"] = {
380		graphic = fbsd_brand,
381	},
382	["none"] = {
383		graphic = none,
384	},
385}
386
387logodefs = {
388	-- Indexed by valid values for loader_logo in loader.conf(5). Valid keys
389	-- are: requires_color (boolean), graphic (table depicting graphic), and
390	-- shift (table containing x and y).
391	["tribute"] = {
392		graphic = fbsd_brand,
393	},
394	["tributebw"] = {
395		graphic = fbsd_brand,
396	},
397	["none"] = {
398		graphic = none,
399		shift = {x = 17, y = 0},
400	},
401}
402
403brand_position = {x = 2, y = 1}
404logo_position = {x = 46, y = 4}
405menu_position = {x = 5, y = 10}
406frame_size = {w = 42, h = 13}
407default_shift = {x = 0, y = 0}
408shift = default_shift
409
410-- Module exports
411drawer.default_brand = 'fbsd'
412drawer.default_color_logodef = 'orb'
413drawer.default_bw_logodef = 'orbbw'
414-- For when things go terribly wrong; this def should be present here in the
415-- drawer module in case it's a filesystem issue.
416drawer.default_fallback_logodef = 'none'
417
418-- These should go away after FreeBSD 13; only available for backwards
419-- compatibility with old logo- files.
420function drawer.addBrand(name, def)
421	branddefs[name] = def
422end
423
424function drawer.addLogo(name, def)
425	logodefs[name] = def
426end
427
428drawer.frame_styles = {
429	-- Indexed by valid values for loader_menu_frame in loader.conf(5).
430	-- All of the keys appearing below must be set for any menu frame style
431	-- added to drawer.frame_styles.
432	["ascii"] = {
433		horizontal	= "-",
434		vertical	= "|",
435		top_left	= "+",
436		bottom_left	= "+",
437		top_right	= "+",
438		bottom_right	= "+",
439	},
440	["single"] = {
441		horizontal	= "\xE2\x94\x80",
442		vertical	= "\xE2\x94\x82",
443		top_left	= "\xE2\x94\x8C",
444		bottom_left	= "\xE2\x94\x94",
445		top_right	= "\xE2\x94\x90",
446		bottom_right	= "\xE2\x94\x98",
447	},
448	["double"] = {
449		horizontal	= "\xE2\x95\x90",
450		vertical	= "\xE2\x95\x91",
451		top_left	= "\xE2\x95\x94",
452		bottom_left	= "\xE2\x95\x9A",
453		top_right	= "\xE2\x95\x97",
454		bottom_right	= "\xE2\x95\x9D",
455	},
456}
457
458function drawer.drawscreen(menudef)
459	-- drawlogo() must go first.
460	-- it determines the positions of other elements
461	drawlogo()
462	drawbrand()
463	drawbox()
464	return drawmenu(menudef)
465end
466
467return drawer
468