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