xref: /freebsd/stand/lua/drawer.lua (revision 2f513db72b034fd5ef7f080b11be5c711c15186a)
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 getBranddef(brand)
65	if brand == nil then
66		return nil
67	end
68	-- Look it up
69	local branddef = branddefs[brand]
70
71	-- Try to pull it in
72	if branddef == nil then
73		try_include('brand-' .. brand)
74		branddef = branddefs[brand]
75	end
76
77	return branddef
78end
79
80local function getLogodef(logo)
81	if logo == nil then
82		return nil
83	end
84	-- Look it up
85	local logodef = logodefs[logo]
86
87	-- Try to pull it in
88	if logodef == nil then
89		try_include('logo-' .. logo)
90		logodef = logodefs[logo]
91	end
92
93	return logodef
94end
95
96local function draw(x, y, logo)
97	for i = 1, #logo do
98		screen.setcursor(x, y + i - 1)
99		printc(logo[i])
100	end
101end
102
103local function drawmenu(menudef)
104	local x = menu_position.x
105	local y = menu_position.y
106
107	x = x + shift.x
108	y = y + shift.y
109
110	-- print the menu and build the alias table
111	local alias_table = {}
112	local entry_num = 0
113	local menu_entries = menudef.entries
114	local effective_line_num = 0
115	if type(menu_entries) == "function" then
116		menu_entries = menu_entries()
117	end
118	for _, e in ipairs(menu_entries) do
119		-- Allow menu items to be conditionally visible by specifying
120		-- a visible function.
121		if e.visible ~= nil and not e.visible() then
122			goto continue
123		end
124		effective_line_num = effective_line_num + 1
125		if e.entry_type ~= core.MENU_SEPARATOR then
126			entry_num = entry_num + 1
127			screen.setcursor(x, y + effective_line_num)
128
129			printc(entry_num .. ". " .. menuEntryName(menudef, e))
130
131			-- fill the alias table
132			alias_table[tostring(entry_num)] = e
133			if e.alias ~= nil then
134				for _, a in ipairs(e.alias) do
135					alias_table[a] = e
136				end
137			end
138		else
139			screen.setcursor(x, y + effective_line_num)
140			printc(menuEntryName(menudef, e))
141		end
142		::continue::
143	end
144	return alias_table
145end
146
147local function defaultframe()
148	if core.isSerialConsole() then
149		return "ascii"
150	end
151	return "double"
152end
153
154local function drawbox()
155	local x = menu_position.x - 3
156	local y = menu_position.y - 1
157	local w = frame_size.w
158	local h = frame_size.h
159
160	local framestyle = loader.getenv("loader_menu_frame") or defaultframe()
161	local framespec = drawer.frame_styles[framestyle]
162	-- If we don't have a framespec for the current frame style, just don't
163	-- draw a box.
164	if framespec == nil then
165		return
166	end
167
168	local hl = framespec.horizontal
169	local vl = framespec.vertical
170
171	local tl = framespec.top_left
172	local bl = framespec.bottom_left
173	local tr = framespec.top_right
174	local br = framespec.bottom_right
175
176	x = x + shift.x
177	y = y + shift.y
178
179	screen.setcursor(x, y); printc(tl)
180	screen.setcursor(x, y + h); printc(bl)
181	screen.setcursor(x + w, y); printc(tr)
182	screen.setcursor(x + w, y + h); printc(br)
183
184	screen.setcursor(x + 1, y)
185	for _ = 1, w - 1 do
186		printc(hl)
187	end
188
189	screen.setcursor(x + 1, y + h)
190	for _ = 1, w - 1 do
191		printc(hl)
192	end
193
194	for i = 1, h - 1 do
195		screen.setcursor(x, y + i)
196		printc(vl)
197		screen.setcursor(x + w, y + i)
198		printc(vl)
199	end
200
201	local menu_header = loader.getenv("loader_menu_title") or
202	    "Welcome to FreeBSD"
203	local menu_header_align = loader.getenv("loader_menu_title_align")
204	local menu_header_x
205
206	if menu_header_align ~= nil then
207		menu_header_align = menu_header_align:lower()
208		if menu_header_align == "left" then
209			-- Just inside the left border on top
210			menu_header_x = x + 1
211		elseif menu_header_align == "right" then
212			-- Just inside the right border on top
213			menu_header_x = x + w - #menu_header
214		end
215	end
216	if menu_header_x == nil then
217		menu_header_x = x + (w / 2) - (#menu_header / 2)
218	end
219	screen.setcursor(menu_header_x, y)
220	printc(menu_header)
221end
222
223local function drawbrand()
224	local x = tonumber(loader.getenv("loader_brand_x")) or
225	    brand_position.x
226	local y = tonumber(loader.getenv("loader_brand_y")) or
227	    brand_position.y
228
229	local branddef = getBranddef(loader.getenv("loader_brand"))
230
231	if branddef == nil then
232		branddef = getBranddef(drawer.default_brand)
233	end
234
235	local graphic = branddef.graphic
236
237	x = x + shift.x
238	y = y + shift.y
239	draw(x, y, graphic)
240end
241
242local function drawlogo()
243	local x = tonumber(loader.getenv("loader_logo_x")) or
244	    logo_position.x
245	local y = tonumber(loader.getenv("loader_logo_y")) or
246	    logo_position.y
247
248	local logo = loader.getenv("loader_logo")
249	local colored = color.isEnabled()
250
251	local logodef = getLogodef(logo)
252
253	if logodef == nil or logodef.graphic == nil or
254	    (not colored and logodef.requires_color) then
255		-- Choose a sensible default
256		if colored then
257			logodef = getLogodef(drawer.default_color_logodef)
258		else
259			logodef = getLogodef(drawer.default_bw_logodef)
260		end
261	end
262
263	if logodef ~= nil and logodef.graphic == none then
264		shift = logodef.shift
265	else
266		shift = default_shift
267	end
268
269	x = x + shift.x
270	y = y + shift.y
271
272	if logodef ~= nil and logodef.shift ~= nil then
273		x = x + logodef.shift.x
274		y = y + logodef.shift.y
275	end
276
277	draw(x, y, logodef.graphic)
278end
279
280fbsd_brand = {
281"  ______               ____   _____ _____  ",
282" |  ____|             |  _ \\ / ____|  __ \\ ",
283" | |___ _ __ ___  ___ | |_) | (___ | |  | |",
284" |  ___| '__/ _ \\/ _ \\|  _ < \\___ \\| |  | |",
285" | |   | | |  __/  __/| |_) |____) | |__| |",
286" | |   | | |    |    ||     |      |      |",
287" |_|   |_|  \\___|\\___||____/|_____/|_____/ "
288}
289none = {""}
290
291menu_name_handlers = {
292	-- Menu name handlers should take the menu being drawn and entry being
293	-- drawn as parameters, and return the name of the item.
294	-- This is designed so that everything, including menu separators, may
295	-- have their names derived differently. The default action for entry
296	-- types not specified here is to use entry.name directly.
297	[core.MENU_SEPARATOR] = function(_, entry)
298		if entry.name ~= nil then
299			if type(entry.name) == "function" then
300				return entry.name()
301			end
302			return entry.name
303		end
304		return ""
305	end,
306	[core.MENU_CAROUSEL_ENTRY] = function(_, entry)
307		local carid = entry.carousel_id
308		local caridx = config.getCarouselIndex(carid)
309		local choices = entry.items
310		if type(choices) == "function" then
311			choices = choices()
312		end
313		if #choices < caridx then
314			caridx = 1
315		end
316		return entry.name(caridx, choices[caridx], choices)
317	end,
318}
319
320branddefs = {
321	-- Indexed by valid values for loader_brand in loader.conf(5). Valid
322	-- keys are: graphic (table depicting graphic)
323	["fbsd"] = {
324		graphic = fbsd_brand,
325	},
326	["none"] = {
327		graphic = none,
328	},
329}
330
331logodefs = {
332	-- Indexed by valid values for loader_logo in loader.conf(5). Valid keys
333	-- are: requires_color (boolean), graphic (table depicting graphic), and
334	-- shift (table containing x and y).
335	["tribute"] = {
336		graphic = fbsd_brand,
337	},
338	["tributebw"] = {
339		graphic = fbsd_brand,
340	},
341	["none"] = {
342		graphic = none,
343		shift = {x = 17, y = 0},
344	},
345}
346
347brand_position = {x = 2, y = 1}
348logo_position = {x = 46, y = 4}
349menu_position = {x = 5, y = 10}
350frame_size = {w = 42, h = 13}
351default_shift = {x = 0, y = 0}
352shift = default_shift
353
354-- Module exports
355drawer.default_brand = 'fbsd'
356drawer.default_color_logodef = 'orb'
357drawer.default_bw_logodef = 'orbbw'
358
359function drawer.addBrand(name, def)
360	branddefs[name] = def
361end
362
363function drawer.addLogo(name, def)
364	logodefs[name] = def
365end
366
367drawer.frame_styles = {
368	-- Indexed by valid values for loader_menu_frame in loader.conf(5).
369	-- All of the keys appearing below must be set for any menu frame style
370	-- added to drawer.frame_styles.
371	["ascii"] = {
372		horizontal	= "-",
373		vertical	= "|",
374		top_left	= "+",
375		bottom_left	= "+",
376		top_right	= "+",
377		bottom_right	= "+",
378	},
379	["single"] = {
380		horizontal	= "\xE2\x94\x80",
381		vertical	= "\xE2\x94\x82",
382		top_left	= "\xE2\x94\x8C",
383		bottom_left	= "\xE2\x94\x94",
384		top_right	= "\xE2\x94\x90",
385		bottom_right	= "\xE2\x94\x98",
386	},
387	["double"] = {
388		horizontal	= "\xE2\x95\x90",
389		vertical	= "\xE2\x95\x91",
390		top_left	= "\xE2\x95\x94",
391		bottom_left	= "\xE2\x95\x9A",
392		top_right	= "\xE2\x95\x97",
393		bottom_right	= "\xE2\x95\x9D",
394	},
395}
396
397function drawer.drawscreen(menudef)
398	-- drawlogo() must go first.
399	-- it determines the positions of other elements
400	drawlogo()
401	drawbrand()
402	drawbox()
403	return drawmenu(menudef)
404end
405
406return drawer
407