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