xref: /freebsd/stand/lua/drawer.lua (revision 25ecdc7d52770caf1c9b44b5ec11f468f6b636f3)
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 drawframe()
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 false
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	if core.isFramebufferConsole() and loader.term_drawrect ~= nil then
231		loader.term_drawrect(x, y, x + w, y + h)
232		return true
233	end
234
235	screen.setcursor(x, y); printc(tl)
236	screen.setcursor(x, y + h); printc(bl)
237	screen.setcursor(x + w, y); printc(tr)
238	screen.setcursor(x + w, y + h); printc(br)
239
240	screen.setcursor(x + 1, y)
241	for _ = 1, w - 1 do
242		printc(hl)
243	end
244
245	screen.setcursor(x + 1, y + h)
246	for _ = 1, w - 1 do
247		printc(hl)
248	end
249
250	for i = 1, h - 1 do
251		screen.setcursor(x, y + i)
252		printc(vl)
253		screen.setcursor(x + w, y + i)
254		printc(vl)
255	end
256	return true
257end
258
259local function drawbox()
260	local x = menu_position.x - 3
261	local y = menu_position.y - 1
262	local w = frame_size.w
263	local menu_header = loader.getenv("loader_menu_title") or
264	    "Welcome to FreeBSD"
265	local menu_header_align = loader.getenv("loader_menu_title_align")
266	local menu_header_x
267
268	x = x + shift.x
269	y = y + shift.y
270
271	if drawframe(x, y, w) == false then
272		return
273	end
274
275	if menu_header_align ~= nil then
276		menu_header_align = menu_header_align:lower()
277		if menu_header_align == "left" then
278			-- Just inside the left border on top
279			menu_header_x = x + 1
280		elseif menu_header_align == "right" then
281			-- Just inside the right border on top
282			menu_header_x = x + w - #menu_header
283		end
284	end
285	if menu_header_x == nil then
286		menu_header_x = x + (w // 2) - (#menu_header // 2)
287	end
288	screen.setcursor(menu_header_x - 1, y)
289	if menu_header ~= "" then
290		printc(" " .. menu_header .. " ")
291	end
292
293end
294
295local function drawbrand()
296	local x = tonumber(loader.getenv("loader_brand_x")) or
297	    brand_position.x
298	local y = tonumber(loader.getenv("loader_brand_y")) or
299	    brand_position.y
300
301	local branddef = getBranddef(loader.getenv("loader_brand"))
302
303	if branddef == nil then
304		branddef = getBranddef(drawer.default_brand)
305	end
306
307	local graphic = branddef.graphic
308
309	x = x + shift.x
310	y = y + shift.y
311	if core.isFramebufferConsole() and
312	    loader.term_putimage ~= nil and
313	    branddef.image ~= nil then
314		if loader.term_putimage(branddef.image, 1, 1, 0, 7, 0)
315		then
316			return true
317		end
318	end
319	draw(x, y, graphic)
320end
321
322local function drawlogo()
323	local x = tonumber(loader.getenv("loader_logo_x")) or
324	    logo_position.x
325	local y = tonumber(loader.getenv("loader_logo_y")) or
326	    logo_position.y
327
328	local logo = loader.getenv("loader_logo")
329	local colored = color.isEnabled()
330
331	local logodef = getLogodef(logo)
332
333	if logodef == nil or logodef.graphic == nil or
334	    (not colored and logodef.requires_color) then
335		-- Choose a sensible default
336		if colored then
337			logodef = getLogodef(drawer.default_color_logodef)
338		else
339			logodef = getLogodef(drawer.default_bw_logodef)
340		end
341
342		-- Something has gone terribly wrong.
343		if logodef == nil then
344			logodef = getLogodef(drawer.default_fallback_logodef)
345		end
346	end
347
348	if logodef ~= nil and logodef.graphic == none then
349		shift = logodef.shift
350	else
351		shift = default_shift
352	end
353
354	x = x + shift.x
355	y = y + shift.y
356
357	if logodef ~= nil and logodef.shift ~= nil then
358		x = x + logodef.shift.x
359		y = y + logodef.shift.y
360	end
361
362	if core.isFramebufferConsole() and
363	    loader.term_putimage ~= nil and
364	    logodef.image ~= nil then
365		local y1 = 15
366
367		if logodef.image_rl ~= nil then
368			y1 = logodef.image_rl
369		end
370		if loader.term_putimage(logodef.image, x, y, 0, y + y1, 0)
371		then
372			return true
373		end
374	end
375	draw(x, y, logodef.graphic)
376end
377
378local function drawitem(func)
379	local console = loader.getenv("console")
380	local c
381
382	for c in string.gmatch(console, "%w+") do
383		loader.setenv("console", c)
384		func()
385	end
386	loader.setenv("console", console)
387end
388
389fbsd_brand = {
390"  ______               ____   _____ _____  ",
391" |  ____|             |  _ \\ / ____|  __ \\ ",
392" | |___ _ __ ___  ___ | |_) | (___ | |  | |",
393" |  ___| '__/ _ \\/ _ \\|  _ < \\___ \\| |  | |",
394" | |   | | |  __/  __/| |_) |____) | |__| |",
395" | |   | | |    |    ||     |      |      |",
396" |_|   |_|  \\___|\\___||____/|_____/|_____/ "
397}
398none = {""}
399
400menu_name_handlers = {
401	-- Menu name handlers should take the menu being drawn and entry being
402	-- drawn as parameters, and return the name of the item.
403	-- This is designed so that everything, including menu separators, may
404	-- have their names derived differently. The default action for entry
405	-- types not specified here is to use entry.name directly.
406	[core.MENU_SEPARATOR] = function(_, entry)
407		if entry.name ~= nil then
408			if type(entry.name) == "function" then
409				return entry.name()
410			end
411			return entry.name
412		end
413		return ""
414	end,
415	[core.MENU_CAROUSEL_ENTRY] = function(_, entry)
416		local carid = entry.carousel_id
417		local caridx = config.getCarouselIndex(carid)
418		local choices = entry.items
419		if type(choices) == "function" then
420			choices = choices()
421		end
422		if #choices < caridx then
423			caridx = 1
424		end
425		return entry.name(caridx, choices[caridx], choices)
426	end,
427}
428
429branddefs = {
430	-- Indexed by valid values for loader_brand in loader.conf(5). Valid
431	-- keys are: graphic (table depicting graphic)
432	["fbsd"] = {
433		graphic = fbsd_brand,
434		image = "/boot/images/freebsd-brand-rev.png",
435	},
436	["none"] = {
437		graphic = none,
438	},
439}
440
441logodefs = {
442	-- Indexed by valid values for loader_logo in loader.conf(5). Valid keys
443	-- are: requires_color (boolean), graphic (table depicting graphic), and
444	-- shift (table containing x and y).
445	["tribute"] = {
446		graphic = fbsd_brand,
447	},
448	["tributebw"] = {
449		graphic = fbsd_brand,
450	},
451	["none"] = {
452		graphic = none,
453		shift = {x = 17, y = 0},
454	},
455}
456
457brand_position = {x = 2, y = 1}
458logo_position = {x = 46, y = 4}
459menu_position = {x = 5, y = 10}
460frame_size = {w = 42, h = 13}
461default_shift = {x = 0, y = 0}
462shift = default_shift
463
464-- Module exports
465drawer.default_brand = 'fbsd'
466drawer.default_color_logodef = 'orb'
467drawer.default_bw_logodef = 'orbbw'
468-- For when things go terribly wrong; this def should be present here in the
469-- drawer module in case it's a filesystem issue.
470drawer.default_fallback_logodef = 'none'
471
472-- These should go away after FreeBSD 13; only available for backwards
473-- compatibility with old logo- files.
474function drawer.addBrand(name, def)
475	branddefs[name] = def
476end
477
478function drawer.addLogo(name, def)
479	logodefs[name] = def
480end
481
482drawer.frame_styles = {
483	-- Indexed by valid values for loader_menu_frame in loader.conf(5).
484	-- All of the keys appearing below must be set for any menu frame style
485	-- added to drawer.frame_styles.
486	["ascii"] = {
487		horizontal	= "-",
488		vertical	= "|",
489		top_left	= "+",
490		bottom_left	= "+",
491		top_right	= "+",
492		bottom_right	= "+",
493	},
494	["single"] = {
495		horizontal	= "\xE2\x94\x80",
496		vertical	= "\xE2\x94\x82",
497		top_left	= "\xE2\x94\x8C",
498		bottom_left	= "\xE2\x94\x94",
499		top_right	= "\xE2\x94\x90",
500		bottom_right	= "\xE2\x94\x98",
501	},
502	["double"] = {
503		horizontal	= "\xE2\x95\x90",
504		vertical	= "\xE2\x95\x91",
505		top_left	= "\xE2\x95\x94",
506		bottom_left	= "\xE2\x95\x9A",
507		top_right	= "\xE2\x95\x97",
508		bottom_right	= "\xE2\x95\x9D",
509	},
510}
511
512function drawer.drawscreen(menudef)
513	-- drawlogo() must go first.
514	-- it determines the positions of other elements
515	drawitem(drawlogo)
516	drawitem(drawbrand)
517	drawitem(drawbox)
518	return drawmenu(menudef)
519end
520
521return drawer
522