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