xref: /freebsd/stand/lua/drawer.lua (revision 71702df6126226b31dc3ec66459388e32b993be1)
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	if string.lower(loader.getenv("loader_menu") or "") == "none" then
170	   return
171	end
172
173	x = x + shift.x
174	y = y + shift.y
175
176	-- print the menu and build the alias table
177	local alias_table = {}
178	local entry_num = 0
179	local menu_entries = menudef.entries
180	local effective_line_num = 0
181	if type(menu_entries) == "function" then
182		menu_entries = menu_entries()
183	end
184	for _, e in ipairs(menu_entries) do
185		-- Allow menu items to be conditionally visible by specifying
186		-- a visible function.
187		if e.visible ~= nil and not e.visible() then
188			goto continue
189		end
190		effective_line_num = effective_line_num + 1
191		if e.entry_type ~= core.MENU_SEPARATOR then
192			entry_num = entry_num + 1
193			screen.setcursor(x, y + effective_line_num)
194
195			printc(entry_num .. ". " .. menuEntryName(menudef, e))
196
197			-- fill the alias table
198			alias_table[tostring(entry_num)] = e
199			if e.alias ~= nil then
200				for _, a in ipairs(e.alias) do
201					alias_table[a] = e
202				end
203			end
204		else
205			screen.setcursor(x, y + effective_line_num)
206			printc(menuEntryName(menudef, e))
207		end
208		::continue::
209	end
210	return alias_table
211end
212
213local function defaultframe()
214	if core.isSerialConsole() then
215		return "ascii"
216	end
217	return "double"
218end
219
220local function gfxenabled()
221	return (loader.getenv("loader_gfx") or "yes"):lower() ~= "no"
222end
223local function gfxcapable()
224	return core.isFramebufferConsole() and gfx.term_putimage
225end
226
227local function drawframe()
228	local x = menu_position.x - 3
229	local y = menu_position.y - 1
230	local w = frame_size.w
231	local h = frame_size.h
232
233	local framestyle = loader.getenv("loader_menu_frame") or defaultframe()
234	local framespec = drawer.frame_styles[framestyle]
235	-- If we don't have a framespec for the current frame style, just don't
236	-- draw a box.
237	if framespec == nil then
238		return false
239	end
240
241	local hl = framespec.horizontal
242	local vl = framespec.vertical
243
244	local tl = framespec.top_left
245	local bl = framespec.bottom_left
246	local tr = framespec.top_right
247	local br = framespec.bottom_right
248
249	x = x + shift.x
250	y = y + shift.y
251
252	if gfxenabled() and gfxcapable() then
253		gfx.term_drawrect(x, y, x + w, y + h)
254		return true
255	end
256
257	screen.setcursor(x, y); printc(tl)
258	screen.setcursor(x, y + h); printc(bl)
259	screen.setcursor(x + w, y); printc(tr)
260	screen.setcursor(x + w, y + h); printc(br)
261
262	screen.setcursor(x + 1, y)
263	for _ = 1, w - 1 do
264		printc(hl)
265	end
266
267	screen.setcursor(x + 1, y + h)
268	for _ = 1, w - 1 do
269		printc(hl)
270	end
271
272	for i = 1, h - 1 do
273		screen.setcursor(x, y + i)
274		printc(vl)
275		screen.setcursor(x + w, y + i)
276		printc(vl)
277	end
278	return true
279end
280
281local function drawbox()
282	local x = menu_position.x - 3
283	local y = menu_position.y - 1
284	local w = frame_size.w
285	local menu_header = loader.getenv("loader_menu_title") or
286	    "Welcome to FreeBSD"
287	local menu_header_align = loader.getenv("loader_menu_title_align")
288	local menu_header_x
289
290	if string.lower(loader.getenv("loader_menu") or "") == "none" then
291	   return
292	end
293
294	x = x + shift.x
295	y = y + shift.y
296
297	if drawframe(x, y, w) == false then
298		return
299	end
300
301	if menu_header_align ~= nil then
302		menu_header_align = menu_header_align:lower()
303		if menu_header_align == "left" then
304			-- Just inside the left border on top
305			menu_header_x = x + 1
306		elseif menu_header_align == "right" then
307			-- Just inside the right border on top
308			menu_header_x = x + w - #menu_header
309		end
310	end
311	if menu_header_x == nil then
312		menu_header_x = x + (w // 2) - (#menu_header // 2)
313	end
314	screen.setcursor(menu_header_x - 1, y)
315	if menu_header ~= "" then
316		printc(" " .. menu_header .. " ")
317	end
318
319end
320
321local function drawbrand()
322	local x = tonumber(loader.getenv("loader_brand_x")) or
323	    brand_position.x
324	local y = tonumber(loader.getenv("loader_brand_y")) or
325	    brand_position.y
326
327	local branddef = getBranddef(loader.getenv("loader_brand"))
328
329	if branddef == nil then
330		branddef = getBranddef(drawer.default_brand)
331	end
332
333	local graphic = branddef.ascii.image
334
335	x = x + shift.x
336	y = y + shift.y
337
338	local gfx_requested = branddef.fb and gfxenabled()
339	if gfx_requested and gfxcapable() then
340		if branddef.fb.shift then
341			x = x + (branddef.fb.shift.x or 0)
342			y = y + (branddef.fb.shift.y or 0)
343		end
344		if gfx.term_putimage(branddef.fb.image, x, y, 0, 7, 0) then
345			return true
346		end
347	elseif branddef.ascii.shift then
348		x = x +	(branddef.ascii.shift.x or 0)
349		y = y + (branddef.ascii.shift.y or 0)
350	end
351	draw(x, y, graphic)
352end
353
354local function drawlogo()
355	local x = tonumber(loader.getenv("loader_logo_x")) or
356	    logo_position.x
357	local y = tonumber(loader.getenv("loader_logo_y")) or
358	    logo_position.y
359
360	local logo = loader.getenv("loader_logo")
361	local colored = color.isEnabled()
362
363	local logodef = getLogodef(logo)
364
365	if logodef == nil or logodef.ascii == nil or
366	    (not colored and logodef.ascii.requires_color) then
367		-- Choose a sensible default
368		if colored then
369			logodef = getLogodef(drawer.default_color_logodef)
370		else
371			logodef = getLogodef(drawer.default_bw_logodef)
372		end
373
374		-- Something has gone terribly wrong.
375		if logodef == nil then
376			logodef = getLogodef(drawer.default_fallback_logodef)
377		end
378	end
379
380	-- This is a special little hack for the "none" logo to re-align the
381	-- menu and the brand to avoid having a lot of extraneous whitespace on
382	-- the right side.
383	if logodef and logodef.ascii.image == none then
384		shift = logodef.shift
385	else
386		shift = default_shift
387	end
388
389	x = x + shift.x
390	y = y + shift.y
391
392	local gfx_requested = logodef.fb and gfxenabled()
393	if gfx_requested and gfxcapable() then
394		local y1 = logodef.fb.width or 15
395
396		if logodef.fb.shift then
397			x = x + (logodef.fb.shift.x or 0)
398			y = y + (logodef.fb.shift.y or 0)
399		end
400		if gfx.term_putimage(logodef.fb.image, x, y, 0, y + y1, 0) then
401			return true
402		end
403	elseif logodef.ascii.shift then
404		x = x + (logodef.ascii.shift.x or 0)
405		y = y + (logodef.ascii.shift.y or 0)
406	end
407
408	draw(x, y, logodef.ascii.image)
409end
410
411local function drawitem(func)
412	local console = loader.getenv("console")
413
414	for c in string.gmatch(console, "%w+") do
415		loader.setenv("console", c)
416		func()
417	end
418	loader.setenv("console", console)
419end
420
421fbsd_brand = {
422"  ______               ____   _____ _____  ",
423" |  ____|             |  _ \\ / ____|  __ \\ ",
424" | |___ _ __ ___  ___ | |_) | (___ | |  | |",
425" |  ___| '__/ _ \\/ _ \\|  _ < \\___ \\| |  | |",
426" | |   | | |  __/  __/| |_) |____) | |__| |",
427" | |   | | |    |    ||     |      |      |",
428" |_|   |_|  \\___|\\___||____/|_____/|_____/ "
429}
430none = {""}
431
432menu_name_handlers = {
433	-- Menu name handlers should take the menu being drawn and entry being
434	-- drawn as parameters, and return the name of the item.
435	-- This is designed so that everything, including menu separators, may
436	-- have their names derived differently. The default action for entry
437	-- types not specified here is to use entry.name directly.
438	[core.MENU_SEPARATOR] = function(_, entry)
439		if entry.name ~= nil then
440			if type(entry.name) == "function" then
441				return entry.name()
442			end
443			return entry.name
444		end
445		return ""
446	end,
447	[core.MENU_CAROUSEL_ENTRY] = function(_, entry)
448		local carid = entry.carousel_id
449		local caridx = config.getCarouselIndex(carid)
450		local choices = entry.items
451		if type(choices) == "function" then
452			choices = choices()
453		end
454		if #choices < caridx then
455			caridx = 1
456		end
457		return entry.name(caridx, choices[caridx], choices)
458	end,
459}
460
461branddefs = {
462	-- Indexed by valid values for loader_brand in loader.conf(5). Valid
463	-- keys are: graphic (table depicting graphic)
464	["fbsd"] = {
465		ascii = {
466			image = fbsd_brand,
467		},
468		fb = {
469			image = "/boot/images/freebsd-brand-rev.png",
470		},
471	},
472	["none"] = {
473		fb = { image = none },
474	},
475}
476
477logodefs = {
478	-- Indexed by valid values for loader_logo in loader.conf(5). Valid keys
479	-- are: requires_color (boolean), graphic (table depicting graphic), and
480	-- shift (table containing x and y).
481	["tribute"] = {
482		ascii = {
483			image = fbsd_brand,
484		},
485	},
486	["tributebw"] = {
487		ascii = {
488			image = fbsd_brand,
489		},
490	},
491	["none"] = {
492		ascii = {
493			image = none,
494		},
495		shift = {x = 17, y = 0},
496	},
497}
498
499brand_position = {x = 2, y = 1}
500logo_position = {x = 40, y = 10}
501menu_position = {x = 5, y = 10}
502frame_size = {w = 39, h = 14}
503default_shift = {x = 0, y = 0}
504shift = default_shift
505
506-- Module exports
507drawer.default_brand = 'fbsd'
508drawer.default_color_logodef = 'orb'
509drawer.default_bw_logodef = 'orbbw'
510-- For when things go terribly wrong; this def should be present here in the
511-- drawer module in case it's a filesystem issue.
512drawer.default_fallback_logodef = 'none'
513
514-- Backwards compatibility shims for previous FreeBSD versions, please document
515-- new additions
516local function adapt_fb_shim(def)
517	-- In FreeBSD 14.x+, we have improved framebuffer support in the loader
518	-- and some graphics may have images that we can actually draw on the
519	-- screen.  Those graphics may come with shifts that are distinct from
520	-- the ASCII version, so we move both ascii and image versions into
521	-- their own tables.
522	if not def.ascii then
523		def.ascii = {
524			image = def.graphic,
525			requires_color = def.requires_color,
526			shift = def.shift,
527		}
528	end
529	if def.image then
530		assert(not def.fb,
531		    "Unrecognized graphic definition format")
532
533		-- Legacy images may have adapted a shift from the ASCII
534		-- version, or perhaps we just didn't care enough to adjust it.
535		-- Steal the shift.
536		def.fb = {
537			image = def.image,
538			width = def.image_rl,
539			shift = def.shift,
540		}
541	end
542	return def
543end
544
545function drawer.addBrand(name, def)
546	branddefs[name] = adapt_fb_shim(def)
547end
548
549function drawer.addLogo(name, def)
550	logodefs[name] = adapt_fb_shim(def)
551end
552
553drawer.frame_styles = {
554	-- Indexed by valid values for loader_menu_frame in loader.conf(5).
555	-- All of the keys appearing below must be set for any menu frame style
556	-- added to drawer.frame_styles.
557	["ascii"] = {
558		horizontal	= "-",
559		vertical	= "|",
560		top_left	= "+",
561		bottom_left	= "+",
562		top_right	= "+",
563		bottom_right	= "+",
564	},
565}
566
567if core.hasUnicode() then
568	-- unicode based framing characters
569	drawer.frame_styles["single"] = {
570		horizontal	= "\xE2\x94\x80",
571		vertical	= "\xE2\x94\x82",
572		top_left	= "\xE2\x94\x8C",
573		bottom_left	= "\xE2\x94\x94",
574		top_right	= "\xE2\x94\x90",
575		bottom_right	= "\xE2\x94\x98",
576	}
577	drawer.frame_styles["double"] = {
578		horizontal	= "\xE2\x95\x90",
579		vertical	= "\xE2\x95\x91",
580		top_left	= "\xE2\x95\x94",
581		bottom_left	= "\xE2\x95\x9A",
582		top_right	= "\xE2\x95\x97",
583		bottom_right	= "\xE2\x95\x9D",
584	}
585else
586	-- non-unicode cons25-style framing characters
587	drawer.frame_styles["single"] = {
588		horizontal	= "\xC4",
589		vertical	= "\xB3",
590		top_left	= "\xDA",
591		bottom_left	= "\xC0",
592		top_right	= "\xBF",
593		bottom_right	= "\xD9",
594        }
595	drawer.frame_styles["double"] = {
596		horizontal	= "\xCD",
597		vertical	= "\xBA",
598		top_left	= "\xC9",
599		bottom_left	= "\xC8",
600		top_right	= "\xBB",
601		bottom_right	= "\xBC",
602	}
603end
604
605function drawer.drawscreen(menudef)
606	-- drawlogo() must go first.
607	-- it determines the positions of other elements
608	drawitem(drawlogo)
609	drawitem(drawbrand)
610	drawitem(drawbox)
611	return drawmenu(menudef)
612end
613
614return drawer
615