dotfiles/.config/lite-xl/libraries/widget/fonts/cache.lua

286 lines
7.6 KiB
Lua

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<integer, string>
---@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