xref: /freebsd/stand/lua/drawer.lua (revision e9e8876a4d6afc1ad5315faaa191b25121a813d7)
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 branddef.shift ~= nil then
312		x = x +	branddef.shift.x
313		y = y + branddef.shift.y
314	end
315
316	if core.isFramebufferConsole() and
317	    loader.term_putimage ~= nil and
318	    branddef.image ~= nil then
319		if loader.term_putimage(branddef.image, 1, 1, 0, 7, 0)
320		then
321			return true
322		end
323	end
324	draw(x, y, graphic)
325end
326
327local function drawlogo()
328	local x = tonumber(loader.getenv("loader_logo_x")) or
329	    logo_position.x
330	local y = tonumber(loader.getenv("loader_logo_y")) or
331	    logo_position.y
332
333	local logo = loader.getenv("loader_logo")
334	local colored = color.isEnabled()
335
336	local logodef = getLogodef(logo)
337
338	if logodef == nil or logodef.graphic == nil or
339	    (not colored and logodef.requires_color) then
340		-- Choose a sensible default
341		if colored then
342			logodef = getLogodef(drawer.default_color_logodef)
343		else
344			logodef = getLogodef(drawer.default_bw_logodef)
345		end
346
347		-- Something has gone terribly wrong.
348		if logodef == nil then
349			logodef = getLogodef(drawer.default_fallback_logodef)
350		end
351	end
352
353	if logodef ~= nil and logodef.graphic == none then
354		shift = logodef.shift
355	else
356		shift = default_shift
357	end
358
359	x = x + shift.x
360	y = y + shift.y
361
362	if logodef ~= nil and logodef.shift ~= nil then
363		x = x + logodef.shift.x
364		y = y + logodef.shift.y
365	end
366
367	if core.isFramebufferConsole() and
368	    loader.term_putimage ~= nil and
369	    logodef.image ~= nil then
370		local y1 = 15
371
372		if logodef.image_rl ~= nil then
373			y1 = logodef.image_rl
374		end
375		if loader.term_putimage(logodef.image, x, y, 0, y + y1, 0)
376		then
377			return true
378		end
379	end
380	draw(x, y, logodef.graphic)
381end
382
383local function drawitem(func)
384	local console = loader.getenv("console")
385
386	for c in string.gmatch(console, "%w+") do
387		loader.setenv("console", c)
388		func()
389	end
390	loader.setenv("console", console)
391end
392
393fbsd_brand = {
394"  ______               ____   _____ _____  ",
395" |  ____|             |  _ \\ / ____|  __ \\ ",
396" | |___ _ __ ___  ___ | |_) | (___ | |  | |",
397" |  ___| '__/ _ \\/ _ \\|  _ < \\___ \\| |  | |",
398" | |   | | |  __/  __/| |_) |____) | |__| |",
399" | |   | | |    |    ||     |      |      |",
400" |_|   |_|  \\___|\\___||____/|_____/|_____/ "
401}
402none = {""}
403
404menu_name_handlers = {
405	-- Menu name handlers should take the menu being drawn and entry being
406	-- drawn as parameters, and return the name of the item.
407	-- This is designed so that everything, including menu separators, may
408	-- have their names derived differently. The default action for entry
409	-- types not specified here is to use entry.name directly.
410	[core.MENU_SEPARATOR] = function(_, entry)
411		if entry.name ~= nil then
412			if type(entry.name) == "function" then
413				return entry.name()
414			end
415			return entry.name
416		end
417		return ""
418	end,
419	[core.MENU_CAROUSEL_ENTRY] = function(_, entry)
420		local carid = entry.carousel_id
421		local caridx = config.getCarouselIndex(carid)
422		local choices = entry.items
423		if type(choices) == "function" then
424			choices = choices()
425		end
426		if #choices < caridx then
427			caridx = 1
428		end
429		return entry.name(caridx, choices[caridx], choices)
430	end,
431}
432
433branddefs = {
434	-- Indexed by valid values for loader_brand in loader.conf(5). Valid
435	-- keys are: graphic (table depicting graphic)
436	["fbsd"] = {
437		graphic = fbsd_brand,
438		image = "/boot/images/freebsd-brand-rev.png",
439	},
440	["none"] = {
441		graphic = none,
442	},
443}
444
445logodefs = {
446	-- Indexed by valid values for loader_logo in loader.conf(5). Valid keys
447	-- are: requires_color (boolean), graphic (table depicting graphic), and
448	-- shift (table containing x and y).
449	["tribute"] = {
450		graphic = fbsd_brand,
451	},
452	["tributebw"] = {
453		graphic = fbsd_brand,
454	},
455	["none"] = {
456		graphic = none,
457		shift = {x = 17, y = 0},
458	},
459}
460
461brand_position = {x = 2, y = 1}
462logo_position = {x = 46, y = 4}
463menu_position = {x = 5, y = 10}
464frame_size = {w = 42, h = 13}
465default_shift = {x = 0, y = 0}
466shift = default_shift
467
468-- Module exports
469drawer.default_brand = 'fbsd'
470drawer.default_color_logodef = 'orb'
471drawer.default_bw_logodef = 'orbbw'
472-- For when things go terribly wrong; this def should be present here in the
473-- drawer module in case it's a filesystem issue.
474drawer.default_fallback_logodef = 'none'
475
476-- These should go away after FreeBSD 13; only available for backwards
477-- compatibility with old logo- files.
478function drawer.addBrand(name, def)
479	branddefs[name] = def
480end
481
482function drawer.addLogo(name, def)
483	logodefs[name] = def
484end
485
486drawer.frame_styles = {
487	-- Indexed by valid values for loader_menu_frame in loader.conf(5).
488	-- All of the keys appearing below must be set for any menu frame style
489	-- added to drawer.frame_styles.
490	["ascii"] = {
491		horizontal	= "-",
492		vertical	= "|",
493		top_left	= "+",
494		bottom_left	= "+",
495		top_right	= "+",
496		bottom_right	= "+",
497	},
498	["single"] = {
499		horizontal	= "\xE2\x94\x80",
500		vertical	= "\xE2\x94\x82",
501		top_left	= "\xE2\x94\x8C",
502		bottom_left	= "\xE2\x94\x94",
503		top_right	= "\xE2\x94\x90",
504		bottom_right	= "\xE2\x94\x98",
505	},
506	["double"] = {
507		horizontal	= "\xE2\x95\x90",
508		vertical	= "\xE2\x95\x91",
509		top_left	= "\xE2\x95\x94",
510		bottom_left	= "\xE2\x95\x9A",
511		top_right	= "\xE2\x95\x97",
512		bottom_right	= "\xE2\x95\x9D",
513	},
514}
515
516function drawer.drawscreen(menudef)
517	-- drawlogo() must go first.
518	-- it determines the positions of other elements
519	drawitem(drawlogo)
520	drawitem(drawbrand)
521	drawitem(drawbox)
522	return drawmenu(menudef)
523end
524
525return drawer
526