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