xref: /freebsd/stand/lua/drawer.lua (revision 4e99f45480598189d49d45a825533a6c9e12f02c)
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
262		-- Something has gone terribly wrong.
263		if logodef == nil then
264			logodef = getLogodef(drawer.default_fallback_logodef)
265		end
266	end
267
268	if logodef ~= nil and logodef.graphic == none then
269		shift = logodef.shift
270	else
271		shift = default_shift
272	end
273
274	x = x + shift.x
275	y = y + shift.y
276
277	if logodef ~= nil and logodef.shift ~= nil then
278		x = x + logodef.shift.x
279		y = y + logodef.shift.y
280	end
281
282	draw(x, y, logodef.graphic)
283end
284
285fbsd_brand = {
286"  ______               ____   _____ _____  ",
287" |  ____|             |  _ \\ / ____|  __ \\ ",
288" | |___ _ __ ___  ___ | |_) | (___ | |  | |",
289" |  ___| '__/ _ \\/ _ \\|  _ < \\___ \\| |  | |",
290" | |   | | |  __/  __/| |_) |____) | |__| |",
291" | |   | | |    |    ||     |      |      |",
292" |_|   |_|  \\___|\\___||____/|_____/|_____/ "
293}
294none = {""}
295
296menu_name_handlers = {
297	-- Menu name handlers should take the menu being drawn and entry being
298	-- drawn as parameters, and return the name of the item.
299	-- This is designed so that everything, including menu separators, may
300	-- have their names derived differently. The default action for entry
301	-- types not specified here is to use entry.name directly.
302	[core.MENU_SEPARATOR] = function(_, entry)
303		if entry.name ~= nil then
304			if type(entry.name) == "function" then
305				return entry.name()
306			end
307			return entry.name
308		end
309		return ""
310	end,
311	[core.MENU_CAROUSEL_ENTRY] = function(_, entry)
312		local carid = entry.carousel_id
313		local caridx = config.getCarouselIndex(carid)
314		local choices = entry.items
315		if type(choices) == "function" then
316			choices = choices()
317		end
318		if #choices < caridx then
319			caridx = 1
320		end
321		return entry.name(caridx, choices[caridx], choices)
322	end,
323}
324
325branddefs = {
326	-- Indexed by valid values for loader_brand in loader.conf(5). Valid
327	-- keys are: graphic (table depicting graphic)
328	["fbsd"] = {
329		graphic = fbsd_brand,
330	},
331	["none"] = {
332		graphic = none,
333	},
334}
335
336logodefs = {
337	-- Indexed by valid values for loader_logo in loader.conf(5). Valid keys
338	-- are: requires_color (boolean), graphic (table depicting graphic), and
339	-- shift (table containing x and y).
340	["tribute"] = {
341		graphic = fbsd_brand,
342	},
343	["tributebw"] = {
344		graphic = fbsd_brand,
345	},
346	["none"] = {
347		graphic = none,
348		shift = {x = 17, y = 0},
349	},
350}
351
352brand_position = {x = 2, y = 1}
353logo_position = {x = 46, y = 4}
354menu_position = {x = 5, y = 10}
355frame_size = {w = 42, h = 13}
356default_shift = {x = 0, y = 0}
357shift = default_shift
358
359-- Module exports
360drawer.default_brand = 'fbsd'
361drawer.default_color_logodef = 'orb'
362drawer.default_bw_logodef = 'orbbw'
363-- For when things go terribly wrong; this def should be present here in the
364-- drawer module in case it's a filesystem issue.
365drawer.default_fallback_logodef = 'none'
366
367function drawer.addBrand(name, def)
368	branddefs[name] = def
369end
370
371function drawer.addLogo(name, def)
372	logodefs[name] = def
373end
374
375drawer.frame_styles = {
376	-- Indexed by valid values for loader_menu_frame in loader.conf(5).
377	-- All of the keys appearing below must be set for any menu frame style
378	-- added to drawer.frame_styles.
379	["ascii"] = {
380		horizontal	= "-",
381		vertical	= "|",
382		top_left	= "+",
383		bottom_left	= "+",
384		top_right	= "+",
385		bottom_right	= "+",
386	},
387	["single"] = {
388		horizontal	= "\xE2\x94\x80",
389		vertical	= "\xE2\x94\x82",
390		top_left	= "\xE2\x94\x8C",
391		bottom_left	= "\xE2\x94\x94",
392		top_right	= "\xE2\x94\x90",
393		bottom_right	= "\xE2\x94\x98",
394	},
395	["double"] = {
396		horizontal	= "\xE2\x95\x90",
397		vertical	= "\xE2\x95\x91",
398		top_left	= "\xE2\x95\x94",
399		bottom_left	= "\xE2\x95\x9A",
400		top_right	= "\xE2\x95\x97",
401		bottom_right	= "\xE2\x95\x9D",
402	},
403}
404
405function drawer.drawscreen(menudef)
406	-- drawlogo() must go first.
407	-- it determines the positions of other elements
408	drawlogo()
409	drawbrand()
410	drawbox()
411	return drawmenu(menudef)
412end
413
414return drawer
415