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