xref: /freebsd/stand/lua/drawer.lua (revision 9b37d84c87e69dabc69d818aa4d2fea718bd8b74)
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 drawframe()
221	local x = menu_position.x - 3
222	local y = menu_position.y - 1
223	local w = frame_size.w
224	local h = frame_size.h
225
226	local framestyle = loader.getenv("loader_menu_frame") or defaultframe()
227	local framespec = drawer.frame_styles[framestyle]
228	-- If we don't have a framespec for the current frame style, just don't
229	-- draw a box.
230	if framespec == nil then
231		return false
232	end
233
234	local hl = framespec.horizontal
235	local vl = framespec.vertical
236
237	local tl = framespec.top_left
238	local bl = framespec.bottom_left
239	local tr = framespec.top_right
240	local br = framespec.bottom_right
241
242	x = x + shift.x
243	y = y + shift.y
244
245	if core.isFramebufferConsole() and gfx.term_drawrect ~= nil then
246		gfx.term_drawrect(x, y, x + w, y + h)
247		return true
248	end
249
250	screen.setcursor(x, y); printc(tl)
251	screen.setcursor(x, y + h); printc(bl)
252	screen.setcursor(x + w, y); printc(tr)
253	screen.setcursor(x + w, y + h); printc(br)
254
255	screen.setcursor(x + 1, y)
256	for _ = 1, w - 1 do
257		printc(hl)
258	end
259
260	screen.setcursor(x + 1, y + h)
261	for _ = 1, w - 1 do
262		printc(hl)
263	end
264
265	for i = 1, h - 1 do
266		screen.setcursor(x, y + i)
267		printc(vl)
268		screen.setcursor(x + w, y + i)
269		printc(vl)
270	end
271	return true
272end
273
274local function drawbox()
275	local x = menu_position.x - 3
276	local y = menu_position.y - 1
277	local w = frame_size.w
278	local menu_header = loader.getenv("loader_menu_title") or
279	    "Welcome to FreeBSD"
280	local menu_header_align = loader.getenv("loader_menu_title_align")
281	local menu_header_x
282
283	if string.lower(loader.getenv("loader_menu") or "") == "none" then
284	   return
285	end
286
287	x = x + shift.x
288	y = y + shift.y
289
290	if drawframe(x, y, w) == false then
291		return
292	end
293
294	if menu_header_align ~= nil then
295		menu_header_align = menu_header_align:lower()
296		if menu_header_align == "left" then
297			-- Just inside the left border on top
298			menu_header_x = x + 1
299		elseif menu_header_align == "right" then
300			-- Just inside the right border on top
301			menu_header_x = x + w - #menu_header
302		end
303	end
304	if menu_header_x == nil then
305		menu_header_x = x + (w // 2) - (#menu_header // 2)
306	end
307	screen.setcursor(menu_header_x - 1, y)
308	if menu_header ~= "" then
309		printc(" " .. menu_header .. " ")
310	end
311
312end
313
314local function drawbrand()
315	local x = tonumber(loader.getenv("loader_brand_x")) or
316	    brand_position.x
317	local y = tonumber(loader.getenv("loader_brand_y")) or
318	    brand_position.y
319
320	local branddef = getBranddef(loader.getenv("loader_brand"))
321
322	if branddef == nil then
323		branddef = getBranddef(drawer.default_brand)
324	end
325
326	local graphic = branddef.graphic
327
328	x = x + shift.x
329	y = y + shift.y
330	if branddef.shift ~= nil then
331		x = x +	branddef.shift.x
332		y = y + branddef.shift.y
333	end
334
335	if core.isFramebufferConsole() and
336	    gfx.term_putimage ~= nil and
337	    branddef.image ~= nil then
338		if gfx.term_putimage(branddef.image, x, y, 0, 7, 0)
339		then
340			return true
341		end
342	end
343	draw(x, y, graphic)
344end
345
346local function drawlogo()
347	local x = tonumber(loader.getenv("loader_logo_x")) or
348	    logo_position.x
349	local y = tonumber(loader.getenv("loader_logo_y")) or
350	    logo_position.y
351
352	local logo = loader.getenv("loader_logo")
353	local colored = color.isEnabled()
354
355	local logodef = getLogodef(logo)
356
357	if logodef == nil or logodef.graphic == nil or
358	    (not colored and logodef.requires_color) then
359		-- Choose a sensible default
360		if colored then
361			logodef = getLogodef(drawer.default_color_logodef)
362		else
363			logodef = getLogodef(drawer.default_bw_logodef)
364		end
365
366		-- Something has gone terribly wrong.
367		if logodef == nil then
368			logodef = getLogodef(drawer.default_fallback_logodef)
369		end
370	end
371
372	if logodef ~= nil and logodef.graphic == none then
373		shift = logodef.shift
374	else
375		shift = default_shift
376	end
377
378	x = x + shift.x
379	y = y + shift.y
380
381	if logodef ~= nil and logodef.shift ~= nil then
382		x = x + logodef.shift.x
383		y = y + logodef.shift.y
384	end
385
386	if core.isFramebufferConsole() and
387	    gfx.term_putimage ~= nil and
388	    logodef.image ~= nil then
389		local y1 = 15
390
391		if logodef.image_rl ~= nil then
392			y1 = logodef.image_rl
393		end
394		if gfx.term_putimage(logodef.image, x, y, 0, y + y1, 0)
395		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