1--
2-- SPDX-License-Identifier: BSD-2-Clause
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
30local color = require("color")
31local config = require("config")
32local core = require("core")
33local screen = require("screen")
34
35local drawer = {}
36
37local fbsd_brand
38local none
39
40local menu_name_handlers
41local branddefs
42local logodefs
43local brand_position
44local logo_position
45local menu_position
46local frame_size
47local default_shift
48local shift
49
50-- Make this code compatible with older loader binaries. We moved the term_*
51-- functions from loader to the gfx. if we're running on an older loader that
52-- has these functions, create aliases for them in gfx. The loader binary might
53-- be so old as to not have them, but in that case, we want to copy the nil
54-- values. The new loader will provide loader.* versions of all the gfx.*
55-- functions for backwards compatibility, so we only define the functions we use
56-- here.
57if gfx == nil then
58	gfx = {}
59	gfx.term_drawrect = loader.term_drawrect
60	gfx.term_putimage = loader.term_putimage
61end
62
63local function menuEntryName(drawing_menu, entry)
64	local name_handler = menu_name_handlers[entry.entry_type]
65
66	if name_handler ~= nil then
67		return name_handler(drawing_menu, entry)
68	end
69	if type(entry.name) == "function" then
70		return entry.name()
71	end
72	return entry.name
73end
74
75local function processFile(gfxname)
76	if gfxname == nil then
77		return false, "Missing filename"
78	end
79
80	local ret = try_include('gfx-' .. gfxname)
81	if ret == nil then
82		return false, "Failed to include gfx-" .. gfxname
83	end
84
85	-- Legacy format
86	if type(ret) ~= "table" then
87		return true
88	end
89
90	for gfxtype, def in pairs(ret) do
91		if gfxtype == "brand" then
92			drawer.addBrand(gfxname, def)
93		elseif gfxtype == "logo" then
94			drawer.addLogo(gfxname, def)
95		else
96			return false, "Unknown graphics type '" .. gfxtype ..
97			    "'"
98		end
99	end
100
101	return true
102end
103
104local function getBranddef(brand)
105	if brand == nil then
106		return nil
107	end
108	-- Look it up
109	local branddef = branddefs[brand]
110
111	-- Try to pull it in
112	if branddef == nil then
113		local res, err = processFile(brand)
114		if not res then
115			-- This fallback should go away after FreeBSD 13.
116			try_include('brand-' .. brand)
117			-- If the fallback also failed, print whatever error
118			-- we encountered in the original processing.
119			if branddefs[brand] == nil then
120				print(err)
121				return nil
122			end
123		end
124
125		branddef = branddefs[brand]
126	end
127
128	return branddef
129end
130
131local function getLogodef(logo)
132	if logo == nil then
133		return nil
134	end
135	-- Look it up
136	local logodef = logodefs[logo]
137
138	-- Try to pull it in
139	if logodef == nil then
140		local res, err = processFile(logo)
141		if not res then
142			-- This fallback should go away after FreeBSD 13.
143			try_include('logo-' .. logo)
144			-- If the fallback also failed, print whatever error
145			-- we encountered in the original processing.
146			if logodefs[logo] == nil then
147				print(err)
148				return nil
149			end
150		end
151
152		logodef = logodefs[logo]
153	end
154
155	return logodef
156end
157
158local function draw(x, y, logo)
159	for i = 1, #logo do
160		screen.setcursor(x, y + i - 1)
161		printc(logo[i])
162	end
163end
164
165local function drawmenu(menudef)
166	local x = menu_position.x
167	local y = menu_position.y
168
169	x = x + shift.x
170	y = y + shift.y
171
172	-- print the menu and build the alias table
173	local alias_table = {}
174	local entry_num = 0
175	local menu_entries = menudef.entries
176	local effective_line_num = 0
177	if type(menu_entries) == "function" then
178		menu_entries = menu_entries()
179	end
180	for _, e in ipairs(menu_entries) do
181		-- Allow menu items to be conditionally visible by specifying
182		-- a visible function.
183		if e.visible ~= nil and not e.visible() then
184			goto continue
185		end
186		effective_line_num = effective_line_num + 1
187		if e.entry_type ~= core.MENU_SEPARATOR then
188			entry_num = entry_num + 1
189			screen.setcursor(x, y + effective_line_num)
190
191			printc(entry_num .. ". " .. menuEntryName(menudef, e))
192
193			-- fill the alias table
194			alias_table[tostring(entry_num)] = e
195			if e.alias ~= nil then
196				for _, a in ipairs(e.alias) do
197					alias_table[a] = e
198				end
199			end
200		else
201			screen.setcursor(x, y + effective_line_num)
202			printc(menuEntryName(menudef, e))
203		end
204		::continue::
205	end
206	return alias_table
207end
208
209local function defaultframe()
210	if core.isSerialConsole() then
211		return "ascii"
212	end
213	return "double"
214end
215
216local function drawframe()
217	local x = menu_position.x - 3
218	local y = menu_position.y - 1
219	local w = frame_size.w
220	local h = frame_size.h
221
222	local framestyle = loader.getenv("loader_menu_frame") or defaultframe()
223	local framespec = drawer.frame_styles[framestyle]
224	-- If we don't have a framespec for the current frame style, just don't
225	-- draw a box.
226	if framespec == nil then
227		return false
228	end
229
230	local hl = framespec.horizontal
231	local vl = framespec.vertical
232
233	local tl = framespec.top_left
234	local bl = framespec.bottom_left
235	local tr = framespec.top_right
236	local br = framespec.bottom_right
237
238	x = x + shift.x
239	y = y + shift.y
240
241	if core.isFramebufferConsole() and gfx.term_drawrect ~= nil then
242		gfx.term_drawrect(x, y, x + w, y + h)
243		return true
244	end
245
246	screen.setcursor(x, y); printc(tl)
247	screen.setcursor(x, y + h); printc(bl)
248	screen.setcursor(x + w, y); printc(tr)
249	screen.setcursor(x + w, y + h); printc(br)
250
251	screen.setcursor(x + 1, y)
252	for _ = 1, w - 1 do
253		printc(hl)
254	end
255
256	screen.setcursor(x + 1, y + h)
257	for _ = 1, w - 1 do
258		printc(hl)
259	end
260
261	for i = 1, h - 1 do
262		screen.setcursor(x, y + i)
263		printc(vl)
264		screen.setcursor(x + w, y + i)
265		printc(vl)
266	end
267	return true
268end
269
270local function drawbox()
271	local x = menu_position.x - 3
272	local y = menu_position.y - 1
273	local w = frame_size.w
274	local menu_header = loader.getenv("loader_menu_title") or
275	    "Welcome to FreeBSD"
276	local menu_header_align = loader.getenv("loader_menu_title_align")
277	local menu_header_x
278
279	x = x + shift.x
280	y = y + shift.y
281
282	if drawframe(x, y, w) == false then
283		return
284	end
285
286	if menu_header_align ~= nil then
287		menu_header_align = menu_header_align:lower()
288		if menu_header_align == "left" then
289			-- Just inside the left border on top
290			menu_header_x = x + 1
291		elseif menu_header_align == "right" then
292			-- Just inside the right border on top
293			menu_header_x = x + w - #menu_header
294		end
295	end
296	if menu_header_x == nil then
297		menu_header_x = x + (w // 2) - (#menu_header // 2)
298	end
299	screen.setcursor(menu_header_x - 1, y)
300	if menu_header ~= "" then
301		printc(" " .. menu_header .. " ")
302	end
303
304end
305
306local function drawbrand()
307	local x = tonumber(loader.getenv("loader_brand_x")) or
308	    brand_position.x
309	local y = tonumber(loader.getenv("loader_brand_y")) or
310	    brand_position.y
311
312	local branddef = getBranddef(loader.getenv("loader_brand"))
313
314	if branddef == nil then
315		branddef = getBranddef(drawer.default_brand)
316	end
317
318	local graphic = branddef.graphic
319
320	x = x + shift.x
321	y = y + shift.y
322	if branddef.shift ~= nil then
323		x = x +	branddef.shift.x
324		y = y + branddef.shift.y
325	end
326
327	if core.isFramebufferConsole() and
328	    gfx.term_putimage ~= nil and
329	    branddef.image ~= nil then
330		if gfx.term_putimage(branddef.image, 1, 1, 0, 7, 0)
331		then
332			return true
333		end
334	end
335	draw(x, y, graphic)
336end
337
338local function drawlogo()
339	local x = tonumber(loader.getenv("loader_logo_x")) or
340	    logo_position.x
341	local y = tonumber(loader.getenv("loader_logo_y")) or
342	    logo_position.y
343
344	local logo = loader.getenv("loader_logo")
345	local colored = color.isEnabled()
346
347	local logodef = getLogodef(logo)
348
349	if logodef == nil or logodef.graphic == nil or
350	    (not colored and logodef.requires_color) then
351		-- Choose a sensible default
352		if colored then
353			logodef = getLogodef(drawer.default_color_logodef)
354		else
355			logodef = getLogodef(drawer.default_bw_logodef)
356		end
357
358		-- Something has gone terribly wrong.
359		if logodef == nil then
360			logodef = getLogodef(drawer.default_fallback_logodef)
361		end
362	end
363
364	if logodef ~= nil and logodef.graphic == none then
365		shift = logodef.shift
366	else
367		shift = default_shift
368	end
369
370	x = x + shift.x
371	y = y + shift.y
372
373	if logodef ~= nil and logodef.shift ~= nil then
374		x = x + logodef.shift.x
375		y = y + logodef.shift.y
376	end
377
378	if core.isFramebufferConsole() and
379	    gfx.term_putimage ~= nil and
380	    logodef.image ~= nil then
381		local y1 = 15
382
383		if logodef.image_rl ~= nil then
384			y1 = logodef.image_rl
385		end
386		if gfx.term_putimage(logodef.image, x, y, 0, y + y1, 0)
387		then
388			return true
389		end
390	end
391	draw(x, y, logodef.graphic)
392end
393
394local function drawitem(func)
395	local console = loader.getenv("console")
396
397	for c in string.gmatch(console, "%w+") do
398		loader.setenv("console", c)
399		func()
400	end
401	loader.setenv("console", console)
402end
403
404fbsd_brand = {
405"  ______               ____   _____ _____  ",
406" |  ____|             |  _ \\ / ____|  __ \\ ",
407" | |___ _ __ ___  ___ | |_) | (___ | |  | |",
408" |  ___| '__/ _ \\/ _ \\|  _ < \\___ \\| |  | |",
409" | |   | | |  __/  __/| |_) |____) | |__| |",
410" | |   | | |    |    ||     |      |      |",
411" |_|   |_|  \\___|\\___||____/|_____/|_____/ "
412}
413none = {""}
414
415menu_name_handlers = {
416	-- Menu name handlers should take the menu being drawn and entry being
417	-- drawn as parameters, and return the name of the item.
418	-- This is designed so that everything, including menu separators, may
419	-- have their names derived differently. The default action for entry
420	-- types not specified here is to use entry.name directly.
421	[core.MENU_SEPARATOR] = function(_, entry)
422		if entry.name ~= nil then
423			if type(entry.name) == "function" then
424				return entry.name()
425			end
426			return entry.name
427		end
428		return ""
429	end,
430	[core.MENU_CAROUSEL_ENTRY] = function(_, entry)
431		local carid = entry.carousel_id
432		local caridx = config.getCarouselIndex(carid)
433		local choices = entry.items
434		if type(choices) == "function" then
435			choices = choices()
436		end
437		if #choices < caridx then
438			caridx = 1
439		end
440		return entry.name(caridx, choices[caridx], choices)
441	end,
442}
443
444branddefs = {
445	-- Indexed by valid values for loader_brand in loader.conf(5). Valid
446	-- keys are: graphic (table depicting graphic)
447	["fbsd"] = {
448		graphic = fbsd_brand,
449		image = "/boot/images/freebsd-brand-rev.png",
450	},
451	["none"] = {
452		graphic = none,
453	},
454}
455
456logodefs = {
457	-- Indexed by valid values for loader_logo in loader.conf(5). Valid keys
458	-- are: requires_color (boolean), graphic (table depicting graphic), and
459	-- shift (table containing x and y).
460	["tribute"] = {
461		graphic = fbsd_brand,
462	},
463	["tributebw"] = {
464		graphic = fbsd_brand,
465	},
466	["none"] = {
467		graphic = none,
468		shift = {x = 17, y = 0},
469	},
470}
471
472brand_position = {x = 2, y = 1}
473logo_position = {x = 46, y = 4}
474menu_position = {x = 5, y = 10}
475frame_size = {w = 42, h = 13}
476default_shift = {x = 0, y = 0}
477shift = default_shift
478
479-- Module exports
480drawer.default_brand = 'fbsd'
481drawer.default_color_logodef = 'orb'
482drawer.default_bw_logodef = 'orbbw'
483-- For when things go terribly wrong; this def should be present here in the
484-- drawer module in case it's a filesystem issue.
485drawer.default_fallback_logodef = 'none'
486
487-- These should go away after FreeBSD 13; only available for backwards
488-- compatibility with old logo- files.
489function drawer.addBrand(name, def)
490	branddefs[name] = def
491end
492
493function drawer.addLogo(name, def)
494	logodefs[name] = def
495end
496
497drawer.frame_styles = {
498	-- Indexed by valid values for loader_menu_frame in loader.conf(5).
499	-- All of the keys appearing below must be set for any menu frame style
500	-- added to drawer.frame_styles.
501	["ascii"] = {
502		horizontal	= "-",
503		vertical	= "|",
504		top_left	= "+",
505		bottom_left	= "+",
506		top_right	= "+",
507		bottom_right	= "+",
508	},
509	["single"] = {
510		horizontal	= "\xE2\x94\x80",
511		vertical	= "\xE2\x94\x82",
512		top_left	= "\xE2\x94\x8C",
513		bottom_left	= "\xE2\x94\x94",
514		top_right	= "\xE2\x94\x90",
515		bottom_right	= "\xE2\x94\x98",
516	},
517	["double"] = {
518		horizontal	= "\xE2\x95\x90",
519		vertical	= "\xE2\x95\x91",
520		top_left	= "\xE2\x95\x94",
521		bottom_left	= "\xE2\x95\x9A",
522		top_right	= "\xE2\x95\x97",
523		bottom_right	= "\xE2\x95\x9D",
524	},
525}
526
527function drawer.drawscreen(menudef)
528	-- drawlogo() must go first.
529	-- it determines the positions of other elements
530	drawitem(drawlogo)
531	drawitem(drawbrand)
532	drawitem(drawbox)
533	return drawmenu(menudef)
534end
535
536return drawer
537