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 104-- Backwards compatibility shims for previous FreeBSD versions, please document 105-- new additions 106local function adapt_fb_shim(def) 107 -- In FreeBSD 14.x+, we have improved framebuffer support in the loader 108 -- and some graphics may have images that we can actually draw on the 109 -- screen. Those graphics may come with shifts that are distinct from 110 -- the ASCII version, so we move both ascii and image versions into 111 -- their own tables. 112 if not def.ascii then 113 def.ascii = { 114 image = def.graphic, 115 requires_color = def.requires_color, 116 shift = def.shift, 117 } 118 end 119 if def.image then 120 assert(not def.fb, 121 "Unrecognized graphic definition format") 122 123 -- Legacy images may have adapted a shift from the ASCII 124 -- version, or perhaps we just didn't care enough to adjust it. 125 -- Steal the shift. 126 def.fb = { 127 image = def.image, 128 width = def.image_rl, 129 shift = def.shift, 130 } 131 end 132 133 def.adapted = true 134 return def 135end 136 137local function getBranddef(brand) 138 if brand == nil then 139 return nil 140 end 141 -- Look it up 142 local branddef = branddefs[brand] 143 144 -- Try to pull it in 145 if branddef == nil then 146 local res, err = processFile(brand) 147 if not res then 148 -- This fallback should go away after FreeBSD 13. 149 try_include('brand-' .. brand) 150 -- If the fallback also failed, print whatever error 151 -- we encountered in the original processing. 152 if branddefs[brand] == nil then 153 print(err) 154 return nil 155 end 156 end 157 158 branddef = branddefs[brand] 159 elseif not branddef.adapted then 160 adapt_fb_shim(branddef) 161 end 162 163 return branddef 164end 165 166local function getLogodef(logo) 167 if logo == nil then 168 return nil 169 end 170 -- Look it up 171 local logodef = logodefs[logo] 172 173 -- Try to pull it in 174 if logodef == nil then 175 local res, err = processFile(logo) 176 if not res then 177 -- This fallback should go away after FreeBSD 13. 178 try_include('logo-' .. logo) 179 -- If the fallback also failed, print whatever error 180 -- we encountered in the original processing. 181 if logodefs[logo] == nil then 182 print(err) 183 return nil 184 end 185 end 186 187 logodef = logodefs[logo] 188 elseif not logodef.adapted then 189 adapt_fb_shim(logodef) 190 end 191 192 return logodef 193end 194 195local function draw(x, y, logo) 196 for i = 1, #logo do 197 screen.setcursor(x, y + i - 1) 198 printc(logo[i]) 199 end 200end 201 202local function drawmenu(menudef) 203 local x = menu_position.x 204 local y = menu_position.y 205 206 if string.lower(loader.getenv("loader_menu") or "") == "none" then 207 return 208 end 209 210 x = x + shift.x 211 y = y + shift.y 212 213 -- print the menu and build the alias table 214 local alias_table = {} 215 local entry_num = 0 216 local menu_entries = menudef.entries 217 local effective_line_num = 0 218 if type(menu_entries) == "function" then 219 menu_entries = menu_entries() 220 end 221 for _, e in ipairs(menu_entries) do 222 -- Allow menu items to be conditionally visible by specifying 223 -- a visible function. 224 if e.visible ~= nil and not e.visible() then 225 goto continue 226 end 227 effective_line_num = effective_line_num + 1 228 if e.entry_type ~= core.MENU_SEPARATOR then 229 entry_num = entry_num + 1 230 screen.setcursor(x, y + effective_line_num) 231 232 printc(entry_num .. ". " .. menuEntryName(menudef, e)) 233 234 -- fill the alias table 235 alias_table[tostring(entry_num)] = e 236 if e.alias ~= nil then 237 for _, a in ipairs(e.alias) do 238 alias_table[a] = e 239 end 240 end 241 else 242 screen.setcursor(x, y + effective_line_num) 243 printc(menuEntryName(menudef, e)) 244 end 245 ::continue:: 246 end 247 return alias_table 248end 249 250local function defaultframe() 251 if core.isSerialConsole() then 252 return "ascii" 253 end 254 return "double" 255end 256 257local function gfxenabled() 258 return (loader.getenv("loader_gfx") or "yes"):lower() ~= "no" 259end 260local function gfxcapable() 261 return core.isFramebufferConsole() and gfx.term_putimage 262end 263 264local function drawframe() 265 local x = menu_position.x - 3 266 local y = menu_position.y - 1 267 local w = frame_size.w 268 local h = frame_size.h 269 270 local framestyle = loader.getenv("loader_menu_frame") or defaultframe() 271 local framespec = drawer.frame_styles[framestyle] 272 -- If we don't have a framespec for the current frame style, just don't 273 -- draw a box. 274 if framespec == nil then 275 return false 276 end 277 278 local hl = framespec.horizontal 279 local vl = framespec.vertical 280 281 local tl = framespec.top_left 282 local bl = framespec.bottom_left 283 local tr = framespec.top_right 284 local br = framespec.bottom_right 285 286 x = x + shift.x 287 y = y + shift.y 288 289 if gfxenabled() and gfxcapable() then 290 gfx.term_drawrect(x, y, x + w, y + h) 291 return true 292 end 293 294 screen.setcursor(x, y); printc(tl) 295 screen.setcursor(x, y + h); printc(bl) 296 screen.setcursor(x + w, y); printc(tr) 297 screen.setcursor(x + w, y + h); printc(br) 298 299 screen.setcursor(x + 1, y) 300 for _ = 1, w - 1 do 301 printc(hl) 302 end 303 304 screen.setcursor(x + 1, y + h) 305 for _ = 1, w - 1 do 306 printc(hl) 307 end 308 309 for i = 1, h - 1 do 310 screen.setcursor(x, y + i) 311 printc(vl) 312 screen.setcursor(x + w, y + i) 313 printc(vl) 314 end 315 return true 316end 317 318local function drawbox() 319 local x = menu_position.x - 3 320 local y = menu_position.y - 1 321 local w = frame_size.w 322 local menu_header = loader.getenv("loader_menu_title") or 323 "Welcome to FreeBSD" 324 local menu_header_align = loader.getenv("loader_menu_title_align") 325 local menu_header_x 326 327 if string.lower(loader.getenv("loader_menu") or "") == "none" then 328 return 329 end 330 331 x = x + shift.x 332 y = y + shift.y 333 334 if drawframe(x, y, w) == false then 335 return 336 end 337 338 if menu_header_align ~= nil then 339 menu_header_align = menu_header_align:lower() 340 if menu_header_align == "left" then 341 -- Just inside the left border on top 342 menu_header_x = x + 1 343 elseif menu_header_align == "right" then 344 -- Just inside the right border on top 345 menu_header_x = x + w - #menu_header 346 end 347 end 348 if menu_header_x == nil then 349 menu_header_x = x + (w // 2) - (#menu_header // 2) 350 end 351 screen.setcursor(menu_header_x - 1, y) 352 if menu_header ~= "" then 353 printc(" " .. menu_header .. " ") 354 end 355 356end 357 358local function drawbrand() 359 local x = tonumber(loader.getenv("loader_brand_x")) or 360 brand_position.x 361 local y = tonumber(loader.getenv("loader_brand_y")) or 362 brand_position.y 363 364 local branddef = getBranddef(loader.getenv("loader_brand")) 365 366 if branddef == nil then 367 branddef = getBranddef(drawer.default_brand) 368 end 369 370 local graphic = branddef.ascii.image 371 372 x = x + shift.x 373 y = y + shift.y 374 375 local gfx_requested = branddef.fb and gfxenabled() 376 if gfx_requested and gfxcapable() then 377 if branddef.fb.shift then 378 x = x + (branddef.fb.shift.x or 0) 379 y = y + (branddef.fb.shift.y or 0) 380 end 381 if gfx.term_putimage(branddef.fb.image, x, y, 0, 7, 0) then 382 return true 383 end 384 elseif branddef.ascii.shift then 385 x = x + (branddef.ascii.shift.x or 0) 386 y = y + (branddef.ascii.shift.y or 0) 387 end 388 draw(x, y, graphic) 389end 390 391local function drawlogo() 392 local x = tonumber(loader.getenv("loader_logo_x")) or 393 logo_position.x 394 local y = tonumber(loader.getenv("loader_logo_y")) or 395 logo_position.y 396 397 local logo = loader.getenv("loader_logo") 398 local colored = color.isEnabled() 399 400 local logodef = getLogodef(logo) 401 402 if logodef == nil or logodef.ascii == nil or 403 (not colored and logodef.ascii.requires_color) then 404 -- Choose a sensible default 405 if colored then 406 logodef = getLogodef(drawer.default_color_logodef) 407 else 408 logodef = getLogodef(drawer.default_bw_logodef) 409 end 410 411 -- Something has gone terribly wrong. 412 if logodef == nil then 413 logodef = getLogodef(drawer.default_fallback_logodef) 414 end 415 end 416 417 -- This is a special little hack for the "none" logo to re-align the 418 -- menu and the brand to avoid having a lot of extraneous whitespace on 419 -- the right side. 420 if logodef and logodef.ascii.image == none then 421 shift = logodef.shift 422 else 423 shift = default_shift 424 end 425 426 x = x + shift.x 427 y = y + shift.y 428 429 local gfx_requested = logodef.fb and gfxenabled() 430 if gfx_requested and gfxcapable() then 431 local y1 = logodef.fb.width or 15 432 433 if logodef.fb.shift then 434 x = x + (logodef.fb.shift.x or 0) 435 y = y + (logodef.fb.shift.y or 0) 436 end 437 if gfx.term_putimage(logodef.fb.image, x, y, 0, y + y1, 0) then 438 return true 439 end 440 elseif logodef.ascii.shift then 441 x = x + (logodef.ascii.shift.x or 0) 442 y = y + (logodef.ascii.shift.y or 0) 443 end 444 445 draw(x, y, logodef.ascii.image) 446end 447 448local function drawitem(func) 449 local console = loader.getenv("console") 450 451 for c in string.gmatch(console, "%w+") do 452 loader.setenv("console", c) 453 func() 454 end 455 loader.setenv("console", console) 456end 457 458fbsd_brand = { 459" ______ ____ _____ _____ ", 460" | ____| | _ \\ / ____| __ \\ ", 461" | |___ _ __ ___ ___ | |_) | (___ | | | |", 462" | ___| '__/ _ \\/ _ \\| _ < \\___ \\| | | |", 463" | | | | | __/ __/| |_) |____) | |__| |", 464" | | | | | | || | | |", 465" |_| |_| \\___|\\___||____/|_____/|_____/ " 466} 467none = {""} 468 469menu_name_handlers = { 470 -- Menu name handlers should take the menu being drawn and entry being 471 -- drawn as parameters, and return the name of the item. 472 -- This is designed so that everything, including menu separators, may 473 -- have their names derived differently. The default action for entry 474 -- types not specified here is to use entry.name directly. 475 [core.MENU_SEPARATOR] = function(_, entry) 476 if entry.name ~= nil then 477 if type(entry.name) == "function" then 478 return entry.name() 479 end 480 return entry.name 481 end 482 return "" 483 end, 484 [core.MENU_CAROUSEL_ENTRY] = function(_, entry) 485 local carid = entry.carousel_id 486 local caridx = config.getCarouselIndex(carid) 487 local choices = entry.items 488 if type(choices) == "function" then 489 choices = choices() 490 end 491 if #choices < caridx then 492 caridx = 1 493 end 494 return entry.name(caridx, choices[caridx], choices) 495 end, 496} 497 498branddefs = { 499 -- Indexed by valid values for loader_brand in loader.conf(5). Valid 500 -- keys are: graphic (table depicting graphic) 501 ["fbsd"] = { 502 ascii = { 503 image = fbsd_brand, 504 }, 505 fb = { 506 image = "/boot/images/freebsd-brand-rev.png", 507 }, 508 }, 509 ["none"] = { 510 ascii = { image = none }, 511 }, 512} 513 514logodefs = { 515 -- Indexed by valid values for loader_logo in loader.conf(5). Valid keys 516 -- are: requires_color (boolean), graphic (table depicting graphic), and 517 -- shift (table containing x and y). 518 ["tribute"] = { 519 ascii = { 520 image = fbsd_brand, 521 }, 522 }, 523 ["tributebw"] = { 524 ascii = { 525 image = fbsd_brand, 526 }, 527 }, 528 ["none"] = { 529 ascii = { 530 image = none, 531 }, 532 shift = {x = 17, y = 0}, 533 }, 534} 535 536brand_position = {x = 2, y = 1} 537logo_position = {x = 40, y = 10} 538menu_position = {x = 5, y = 10} 539frame_size = {w = 39, h = 14} 540default_shift = {x = 0, y = 0} 541shift = default_shift 542 543-- Module exports 544drawer.default_brand = 'fbsd' 545drawer.default_color_logodef = 'orb' 546drawer.default_bw_logodef = 'orbbw' 547-- For when things go terribly wrong; this def should be present here in the 548-- drawer module in case it's a filesystem issue. 549drawer.default_fallback_logodef = 'none' 550 551function drawer.addBrand(name, def) 552 branddefs[name] = adapt_fb_shim(def) 553end 554 555function drawer.addLogo(name, def) 556 logodefs[name] = adapt_fb_shim(def) 557end 558 559drawer.frame_styles = { 560 -- Indexed by valid values for loader_menu_frame in loader.conf(5). 561 -- All of the keys appearing below must be set for any menu frame style 562 -- added to drawer.frame_styles. 563 ["ascii"] = { 564 horizontal = "-", 565 vertical = "|", 566 top_left = "+", 567 bottom_left = "+", 568 top_right = "+", 569 bottom_right = "+", 570 }, 571} 572 573if core.hasUnicode() then 574 -- unicode based framing characters 575 drawer.frame_styles["single"] = { 576 horizontal = "\xE2\x94\x80", 577 vertical = "\xE2\x94\x82", 578 top_left = "\xE2\x94\x8C", 579 bottom_left = "\xE2\x94\x94", 580 top_right = "\xE2\x94\x90", 581 bottom_right = "\xE2\x94\x98", 582 } 583 drawer.frame_styles["double"] = { 584 horizontal = "\xE2\x95\x90", 585 vertical = "\xE2\x95\x91", 586 top_left = "\xE2\x95\x94", 587 bottom_left = "\xE2\x95\x9A", 588 top_right = "\xE2\x95\x97", 589 bottom_right = "\xE2\x95\x9D", 590 } 591else 592 -- non-unicode cons25-style framing characters 593 drawer.frame_styles["single"] = { 594 horizontal = "\xC4", 595 vertical = "\xB3", 596 top_left = "\xDA", 597 bottom_left = "\xC0", 598 top_right = "\xBF", 599 bottom_right = "\xD9", 600 } 601 drawer.frame_styles["double"] = { 602 horizontal = "\xCD", 603 vertical = "\xBA", 604 top_left = "\xC9", 605 bottom_left = "\xC8", 606 top_right = "\xBB", 607 bottom_right = "\xBC", 608 } 609end 610 611function drawer.drawscreen(menudef) 612 -- drawlogo() must go first. 613 -- it determines the positions of other elements 614 drawitem(drawlogo) 615 drawitem(drawbrand) 616 drawitem(drawbox) 617 return drawmenu(menudef) 618end 619 620return drawer 621