local core = require "core" local common = require "core.common" local Object = require "core.object" local FontInfo = require "libraries.widget.fonts.info" ---@class widget.fonts.cache : core.object ---@field fontinfo widget.fonts.info ---@field found integer ---@field found_monospaced integer ---@field building boolean ---@field monosppaced boolean ---@field searching_monospaced boolean ---@field fontdirs table ---@field fonts widget.fonts.data[] local FontCache = Object:extend() ---Constructor function FontCache:new() self.fontinfo = FontInfo() self.fontdirs = {} self.fonts = {} self.loaded_fonts = {} self.found = 0 self.found_monospaced = 0 self.building = false self.searching_monospaced = false self.monospaced = false table.insert(self.fontdirs, USERDIR .. "/fonts") table.insert(self.fontdirs, DATADIR .. "/fonts") if PLATFORM == "Windows" then table.insert(self.fontdirs, HOME .. PATHSEP .. "AppData\\Local\\Microsoft\\Windows\\Fonts" ) table.insert(self.fontdirs, os.getenv("SYSTEMROOT") .. PATHSEP .. "Fonts" ) elseif PLATFORM == "Mac OS X" then table.insert(self.fontdirs, HOME .. "/Library/Fonts") table.insert(self.fontdirs, "/Library/Fonts") table.insert(self.fontdirs, "/System/Library/Fonts") else table.insert(self.fontdirs, HOME .. "/.local/share/fonts") table.insert(self.fontdirs, HOME .. "/.fonts") table.insert(self.fontdirs, "/usr/local/share/fonts") table.insert(self.fontdirs, "/usr/share/fonts") end if not self:load_cache() then self:build() elseif not self.monospaced then self:verify_monospaced() end end ---Check if the cache is already building. ---@return boolean building function FontCache:is_building() if self.building or self.searching_monospaced then return true end return false end ---Build the font cache and save it. ---@return boolean started False if cache is already been built function FontCache:build() if self:is_building() then core.log_quiet("The font cache is already been generated, please wait.") return false end self.found = 0 self.building = true self.monospaced = false self.loaded_fonts = {} core.log_quiet("Generating font cache...") local start_time = system.get_time() local this = self core.add_thread(function() for _, dir in ipairs(this.fontdirs) do this:scan_dir(dir) end this:save_cache() this.building = false this.loaded_fonts = {} core.log_quiet( "Font cache generated in %.1fs for %s fonts!", system.get_time() - start_time, tostring(this.found) ) self:verify_monospaced() end) return true end ---Clear current font cache and rebuild it. ---@return boolean started False if cache is already been built function FontCache:rebuild() if self:is_building() then core.log_quiet("The font cache is already been generated, please wait.") return false end local fontcache_file = USERDIR .. "/font_cache.lua" local file = io.open(fontcache_file, "r") if file ~= nil then file:close() os.remove(fontcache_file) end self.fonts = {} self.loaded_fonts = {} self.found = 0 self.found_monospaced = 0 return self:build() end ---Scan a directory for valid font files and load them into the cache. ---@param path string ---@param run_count? integer function FontCache:scan_dir(path, run_count) run_count = run_count or 1 local can_yield = coroutine.running() local list = system.list_dir(path) if list then for _, name in pairs(list) do if name:match("%.[tToO][tT][fFcC]$") and not self.loaded_fonts[name] then -- prevent loading of duplicate files self.loaded_fonts[name] = true local font_path = path .. PATHSEP .. name local read, errmsg = self.fontinfo:read(font_path) if read then local font_data font_data, errmsg = self.fontinfo:get_data() if font_data then table.insert(self.fonts, font_data) self.found = self.found + 1 else io.stderr:write( "Error: " .. path .. PATHSEP .. name .. "\n" .. " " .. errmsg .. "\n" ) end else io.stderr:write( "Error: " .. path .. PATHSEP .. name .. "\n" .. " " .. errmsg .. "\n" ) end if can_yield and run_count % 100 == 0 then coroutine.yield() end else self:scan_dir(path .. PATHSEP .. name, run_count) end run_count = run_count + 1 end end end ---Search and mark monospaced fonts on currently loaded cache and save it. function FontCache:verify_monospaced() if self:is_building() then core.log_quiet("The monospaced verification is already running, please wait.") return end self.found_monospaced = 0 self.searching_monospaced = true self.monospaced = false core.log_quiet("Finding monospaced fonts...") local start_time = system.get_time() local this = self core.add_thread(function() for _, font in ipairs(this.fonts) do if not font.monospace then FontInfo.check_is_monospace(font) end if font.monospace then this.found_monospaced = this.found_monospaced + 1 end coroutine.yield() end this.monospaced = true this:save_cache() this.searching_monospaced = false core.log_quiet( "Found %s monospaced fonts in %.1fs!", tostring(this.found_monospaced), system.get_time() - start_time ) end) end ---Load font cache from persistent file for faster startup time. function FontCache:load_cache() local ok, t = pcall(dofile, USERDIR .. "/font_cache.lua") if ok then self.fonts = t.fonts self.monospaced = t.monospaced self.found = t.found self.found_monospaced = t.found_monospaced return true end return false end ---Store current font cache to persistent file. function FontCache:save_cache() local fp = io.open(USERDIR .. "/font_cache.lua", "w") if fp then local output = "{\n" .. "found = "..tostring(self.found)..",\n" .. "found_monospaced = "..tostring(self.found_monospaced)..",\n" .. "monospaced = "..tostring(self.monospaced)..",\n" .. "[\"fonts\"] = " .. common.serialize( self.fonts, { pretty = true, escape = true, sort = true, initial_indent = 1 } ):gsub("^%s+", "") .. "\n}\n" fp:write("return ", output) fp:close() end end ---Search for a font and return the best match. ---@param name string ---@param style? widget.fonts.style ---@param monospaced? boolean ---@return widget.fonts.data? font_data ---@return string? errmsg function FontCache:search(name, style, monospaced) if #self.fonts == 0 then return nil, "the font cache needs to be rebuilt" end style = style or "regular" name = name:ulower() style = style:ulower() if name == "monospace" then name = "mono" monospaced = true end if not self.monospaced then monospaced = false end ---@type widget.fonts.data local fontdata = nil local prev_score = 0 for _, font in ipairs(self.fonts) do if not monospaced or (monospaced and font.monospace) then local score = system.fuzzy_match( font.fullname:ulower(), name .. " " .. style, false ) if score ~= nil and (score > prev_score or prev_score == 0) then fontdata = font prev_score = score end end end if fontdata then local fontfile = io.open(fontdata.path, "r") if not fontfile then return nil, "found font file does not exists, cache is outdated" else fontfile:close() end else return nil, "no matching font found" end return fontdata end return FontCache