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