Initial commit
This commit is contained in:
commit
209ba130c0
4852 changed files with 1517959 additions and 0 deletions
286
.config/lite-xl/libraries/widget/fonts/cache.lua
Normal file
286
.config/lite-xl/libraries/widget/fonts/cache.lua
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
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
|
||||
547
.config/lite-xl/libraries/widget/fonts/info.lua
Normal file
547
.config/lite-xl/libraries/widget/fonts/info.lua
Normal file
|
|
@ -0,0 +1,547 @@
|
|||
-- Based on the code from:
|
||||
-- https://gist.github.com/zr-tex8r/1969061a025fa4fc5486c9c28460f48e
|
||||
|
||||
local Object = require "core.object"
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Class Declarations
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
---@class widget.fonts.cdata : core.object
|
||||
---@field private data string
|
||||
---@field private position integer
|
||||
local FontCDATA = Object:extend()
|
||||
|
||||
---@class widget.fonts.reader : core.object
|
||||
---@field private file file*
|
||||
---@field private path string
|
||||
local FontReader = Object:extend()
|
||||
|
||||
---@class widget.fonts.data
|
||||
---@field public path string
|
||||
---@field public id number @Numerical id of the font
|
||||
---@field public type '"ttc"' | '"ttf"' | '"otf"'
|
||||
---@field public copyright string
|
||||
---@field public family string
|
||||
---@field public subfamily '"Regular"' | '"Bold"' | '"Italic"' | '"Bold Italic"'
|
||||
---@field public fullname string
|
||||
---@field public version string
|
||||
---@field public psname string
|
||||
---@field public url string
|
||||
---@field public license string
|
||||
---@field public tfamily string
|
||||
---@field public tsubfamily '"Regular"' | '"Bold"' | '"Italic"' | '"Bold Italic"'
|
||||
---@field public wwsfamily string
|
||||
---@field public wwssubfamily string
|
||||
---@field public monospace boolean
|
||||
|
||||
---@class widget.fonts.info : core.object
|
||||
---@field private reader widget.fonts.reader
|
||||
---@field public path string @Path of the font file
|
||||
---@field public data widget.fonts.data[] @Holds the metadata for each of the embedded fonts
|
||||
local FontInfo = Object:extend()
|
||||
|
||||
---@alias widget.fonts.style
|
||||
---|>'"regular"'
|
||||
---| '"bold"'
|
||||
---| '"italic"'
|
||||
---| '"bold italic"'
|
||||
---| '"thin"'
|
||||
---| '"medium"'
|
||||
---| '"light"'
|
||||
---| '"black"'
|
||||
---| '"condensed"'
|
||||
---| '"oblique"'
|
||||
---| '"bold oblique"'
|
||||
---| '"extra nold"'
|
||||
---| '"Extra bold italic"'
|
||||
---| '"bold condensed"'
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- FontCDATA Implementation
|
||||
--------------------------------------------------------------------------------
|
||||
function FontCDATA:new(data)
|
||||
self.data = data
|
||||
self.position = 0
|
||||
end
|
||||
|
||||
function FontCDATA:__tostring()
|
||||
return "cdata(pos="..self.position..")"
|
||||
end
|
||||
|
||||
function FontCDATA:pos(p)
|
||||
if not p then return self.position end
|
||||
self.position = p
|
||||
return self
|
||||
end
|
||||
|
||||
function FontCDATA:unum(b)
|
||||
local v, data = 0, self.data
|
||||
assert(#data >= self.position + b, 11)
|
||||
for _ = 1, b do
|
||||
self.position = self.position + 1
|
||||
v = v * 256 + data:byte(self.position)
|
||||
end
|
||||
return v
|
||||
end
|
||||
|
||||
function FontCDATA:setunum(b, v)
|
||||
local t, data = {}, self.data
|
||||
t[1] = data:sub(1, self.position)
|
||||
self.position = self.position + b
|
||||
assert(#data >= self.position, 12)
|
||||
t[b + 2] = data:sub(self.position + 1)
|
||||
for i = 1, b do
|
||||
t[b + 2 - i] = string.char(v % 256)
|
||||
v = math.floor(v / 256)
|
||||
end
|
||||
self.data = table.concat(t, '')
|
||||
return self
|
||||
end
|
||||
|
||||
function FontCDATA:str(b)
|
||||
local data = self.data
|
||||
self.position = self.position + b
|
||||
assert(#data >= self.position, 13)
|
||||
return data:sub(self.position - b + 1, self.position)
|
||||
end
|
||||
|
||||
function FontCDATA:setstr(s)
|
||||
local t, data = {}, self.data
|
||||
t[1], t[2] = data:sub(1, self.position), s
|
||||
self.position = self.position + #s
|
||||
assert(#data >= self.position, 14)
|
||||
t[3] = data:sub(self.position + 1)
|
||||
self.data = table.concat(t, '')
|
||||
return self
|
||||
end
|
||||
|
||||
function FontCDATA:ushort()
|
||||
return self:unum(2)
|
||||
end
|
||||
|
||||
function FontCDATA:ulong()
|
||||
return self:unum(4)
|
||||
end
|
||||
|
||||
function FontCDATA:setulong(v)
|
||||
return self:setunum(4, v)
|
||||
end
|
||||
|
||||
function FontCDATA:ulongs(num)
|
||||
local t = {}
|
||||
for i = 1, num do
|
||||
t[i] = self:unum(4)
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- FontReader Implementation
|
||||
--------------------------------------------------------------------------------
|
||||
function FontReader:new(font_path)
|
||||
local file, errmsg = io.open(font_path, "rb")
|
||||
assert(file, errmsg)
|
||||
self.file = file
|
||||
self.path = font_path
|
||||
end
|
||||
|
||||
function FontReader:__gc()
|
||||
if self.file then
|
||||
self.file:close()
|
||||
end
|
||||
end
|
||||
|
||||
function FontReader:__tostring()
|
||||
return "reader("..self.path..")"
|
||||
end
|
||||
|
||||
---@param offset integer
|
||||
---@param len integer
|
||||
---@return widget.fonts.cdata?
|
||||
---@return string|nil errmsg
|
||||
function FontReader:cdata(offset, len)
|
||||
local data, errmsg = self:read(offset, len)
|
||||
if data then
|
||||
return FontCDATA(data)
|
||||
end
|
||||
return nil, errmsg
|
||||
end
|
||||
|
||||
function FontReader:read(offset, len)
|
||||
self.file:seek("set", offset)
|
||||
local data = self.file:read(len)
|
||||
if data:len() ~= len then
|
||||
return nil, "failed reading font data"
|
||||
end
|
||||
return data
|
||||
end
|
||||
|
||||
function FontReader:close()
|
||||
self.file:close()
|
||||
self.file = nil
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- FontInfo Helper Functions
|
||||
--------------------------------------------------------------------------------
|
||||
-- speeds up function lookups
|
||||
local floor, ceil = math.floor, math.ceil
|
||||
|
||||
local function div(x, y)
|
||||
return floor(x / y), x % y
|
||||
end
|
||||
|
||||
local function utf16betoutf8(src)
|
||||
local s, d = { tostring(src):byte(1, -1) }, {}
|
||||
for i = 1, #s - 1, 2 do
|
||||
local c = s[i] * 256 + s[i+1]
|
||||
if c < 0x80 then d[#d+1] = c
|
||||
elseif c < 0x800 then
|
||||
local x, y = div(c, 0x40)
|
||||
d[#d+1] = x + 0xC0; d[#d+1] = y + 0x80
|
||||
elseif c < 0x10000 then
|
||||
local x, y, z = div(c, 0x1000); y, z = div(y, 0x40)
|
||||
d[#d+1] = x + 0xE0; d[#d+1] = y + 0x80; d[#d+1] = z + 0x80
|
||||
else
|
||||
assert(nil)
|
||||
end
|
||||
end
|
||||
return string.char(table.unpack(d))
|
||||
end
|
||||
|
||||
local file_type = {
|
||||
[0x74746366] = 'ttc',
|
||||
[0x10000] = 'ttf',
|
||||
[0x4F54544F] = 'otf',
|
||||
[1008813135] = 'ttc'
|
||||
}
|
||||
|
||||
---@param reader widget.fonts.reader
|
||||
local function otf_offset(reader)
|
||||
local cd, errmsg = reader:cdata(0, 12)
|
||||
if not cd then
|
||||
return nil, errmsg
|
||||
end
|
||||
local tag = cd:ulong()
|
||||
local ftype = file_type[tag];
|
||||
if ftype == 'ttc' then
|
||||
local ver = cd:ulong();
|
||||
local num = cd:ulong();
|
||||
cd, errmsg = reader:cdata(12, 4 * num)
|
||||
if not cd then
|
||||
return nil, errmsg
|
||||
end
|
||||
local res = cd:ulongs(num);
|
||||
return res
|
||||
elseif ftype == 'otf' or ftype == 'ttf' then
|
||||
return { 0 }
|
||||
else
|
||||
return nil, string.format("unknown file tag: %s", tag)
|
||||
end
|
||||
end
|
||||
|
||||
---@param reader widget.fonts.reader
|
||||
---@param fofs integer
|
||||
---@param ntbl integer
|
||||
local function otf_name_table(reader, fofs, ntbl)
|
||||
local cd_d = reader:cdata(fofs + 12, 16 * ntbl)
|
||||
if not cd_d then
|
||||
return nil, "error reading names table"
|
||||
end
|
||||
for _ = 1, ntbl do
|
||||
local t = {-- tag, csum, ofs, len
|
||||
cd_d:str(4), cd_d:ulong(), cd_d:ulong(), cd_d:ulong()
|
||||
}
|
||||
if t[1] == 'name' then
|
||||
return reader:cdata(t[3], ceil(t[4] / 4) * 4)
|
||||
end
|
||||
end
|
||||
return nil, "name table is missing"
|
||||
end
|
||||
|
||||
---@param cdata widget.fonts.cdata
|
||||
local function otf_name_records(cdata)
|
||||
local nfmt, nnum, nofs = cdata:ushort(), cdata:ushort(), cdata:ushort()
|
||||
assert(nfmt == 0, string.format("unsupported name table format: %s", nfmt))
|
||||
local nr = {}
|
||||
for i = 1, nnum do
|
||||
nr[i] = { -- pid, eid, langid, nameid, len, ofs
|
||||
cdata:ushort(), cdata:ushort(), cdata:ushort(),
|
||||
cdata:ushort(), cdata:ushort(), cdata:ushort() + nofs
|
||||
}
|
||||
end
|
||||
return nr
|
||||
end
|
||||
|
||||
---@param cdata widget.fonts.cdata
|
||||
local function otf_name(cdata, nr, nameid)
|
||||
local function seek(pid, eid, lid)
|
||||
for i = 1, #nr do
|
||||
local t = nr[i]
|
||||
local ok = (t[4] == nameid and t[1] == pid and t[2] == eid and
|
||||
t[3] == lid)
|
||||
if ok then return t end
|
||||
end
|
||||
end
|
||||
|
||||
local rec = seek(3, 1, 0x409)
|
||||
or seek(3, 10, 0x409)
|
||||
or seek(1, 0, 0) or seek(0, 3, 0)
|
||||
or seek(0, 4, 0) or seek(0, 6, 0)
|
||||
|
||||
if not rec then return '' end
|
||||
local s = cdata:pos(rec[6]):str(rec[5])
|
||||
return (rec[1] == 3) and utf16betoutf8(s) or s
|
||||
end
|
||||
|
||||
---@param reader widget.fonts.reader
|
||||
local function otf_list(reader, fid, fofs)
|
||||
local cd_fh, errmsg = reader:cdata(fofs, 12)
|
||||
if not cd_fh then
|
||||
return nil, errmsg
|
||||
end
|
||||
|
||||
local tag = cd_fh:ulong()
|
||||
local ntbl = cd_fh:ushort()
|
||||
|
||||
local cd_n = nil
|
||||
cd_n, errmsg = otf_name_table(reader, fofs, ntbl)
|
||||
if not cd_n then
|
||||
return nil, errmsg
|
||||
end
|
||||
|
||||
local ext = { id = fid; type = file_type[tag] or '' }
|
||||
|
||||
local nr = nil
|
||||
nr, errmsg = otf_name_records(cd_n)
|
||||
if not nr then
|
||||
return nil, errmsg
|
||||
end
|
||||
|
||||
local output = {
|
||||
id = ext.id,
|
||||
type = ext.type,
|
||||
copyright = otf_name(cd_n, nr, 0),
|
||||
family = otf_name(cd_n, nr, 1),
|
||||
subfamily = otf_name(cd_n, nr, 2),
|
||||
fullname = otf_name(cd_n, nr, 4),
|
||||
version = otf_name(cd_n, nr, 5),
|
||||
psname = otf_name(cd_n, nr, 6),
|
||||
url = otf_name(cd_n, nr, 11),
|
||||
license = otf_name(cd_n, nr, 13),
|
||||
tfamily = otf_name(cd_n, nr, 16),
|
||||
tsubfamily = otf_name(cd_n, nr, 17),
|
||||
}
|
||||
|
||||
return output
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- FontInfo Implementation
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
---Helper function to check and update a font monospace attribute.
|
||||
---@param font_data widget.fonts.data
|
||||
---@return boolean checked
|
||||
---@return string? errmsg
|
||||
function FontInfo.check_is_monospace(font_data)
|
||||
if font_data then
|
||||
local loaded, fontren = pcall(renderer.font.load, font_data.path, 8, {})
|
||||
if not loaded then
|
||||
return false, "could not load font"
|
||||
else
|
||||
if fontren:get_width("|") == fontren:get_width("w") then
|
||||
font_data.monospace = true
|
||||
else
|
||||
font_data.monospace = false
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---Constructor
|
||||
---@param font_path? string
|
||||
function FontInfo:new(font_path)
|
||||
if type(font_path) == "string" then
|
||||
self:read(font_path)
|
||||
else
|
||||
self.data = {}
|
||||
self.path = ""
|
||||
self.last_error = "no font given"
|
||||
end
|
||||
end
|
||||
|
||||
local function fontinfo_read_native(self, font_path)
|
||||
---@type widget.fonts.data
|
||||
local font
|
||||
---@type string?
|
||||
local errmsg
|
||||
|
||||
---@diagnostic disable-next-line
|
||||
font, errmsg = renderer.font.get_metadata(font_path)
|
||||
|
||||
if not font then
|
||||
self.last_error = errmsg
|
||||
return font, errmsg
|
||||
end
|
||||
|
||||
local add = true
|
||||
local family = nil
|
||||
if font.tfamily then
|
||||
family = font.tfamily
|
||||
elseif font.family then
|
||||
family = font.family
|
||||
end
|
||||
|
||||
local subfamily = nil
|
||||
if font.tsubfamily then
|
||||
subfamily = font.tsubfamily -- sometimes tsubfamily includes more styles
|
||||
elseif font.subfamily then
|
||||
subfamily = font.subfamily
|
||||
end
|
||||
|
||||
-- fix font meta data or discard if empty
|
||||
if family and subfamily then
|
||||
font.fullname = family .. " " .. subfamily
|
||||
elseif font.fullname and family and not font.fullname:ufind(family, 1, true) then
|
||||
font.fullname = font.fullname .. " " .. family
|
||||
elseif not font.fullname and family then
|
||||
font.fullname = family
|
||||
else
|
||||
self.last_error = "font metadata is empty"
|
||||
add = false
|
||||
end
|
||||
|
||||
if add then
|
||||
table.insert(self.data, font)
|
||||
else
|
||||
return nil, self.last_error
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function fontinfo_read_nonnative(self, font_path)
|
||||
self.reader = FontReader(font_path)
|
||||
|
||||
local tofs, errmsg = otf_offset(self.reader)
|
||||
|
||||
if not tofs then
|
||||
self.last_error = errmsg
|
||||
return nil, errmsg
|
||||
end
|
||||
|
||||
local data = nil
|
||||
for i = 1, #tofs do
|
||||
data, errmsg = otf_list(self.reader, i - 1, tofs[i])
|
||||
if data then
|
||||
table.insert(self.data, data)
|
||||
else
|
||||
self.last_error = errmsg
|
||||
return nil, errmsg
|
||||
end
|
||||
end
|
||||
|
||||
if self.data[1] then
|
||||
local font = self.data[1]
|
||||
|
||||
local family = nil
|
||||
if font.tfamily ~= "" then
|
||||
family = font.tfamily
|
||||
elseif font.family ~= "" then
|
||||
family = font.family
|
||||
end
|
||||
|
||||
local subfamily = nil
|
||||
if font.tsubfamily ~= "" then
|
||||
subfamily = font.tsubfamily -- sometimes tsubfamily includes more styles
|
||||
elseif font.subfamily ~= "" then
|
||||
subfamily = font.subfamily
|
||||
end
|
||||
|
||||
-- fix font meta data or discard if empty
|
||||
if family and subfamily then
|
||||
font.fullname = family .. " " .. subfamily
|
||||
elseif font.fullname ~= "" and family and not font.fullname:ufind(family, 1, true) then
|
||||
font.fullname = font.fullname .. " " .. family
|
||||
elseif font.fullname == "" and family then
|
||||
font.fullname = family
|
||||
else
|
||||
self.data = {}
|
||||
self.last_error = "font metadata is empty"
|
||||
return nil, self.last_error
|
||||
end
|
||||
end
|
||||
|
||||
self.reader:close()
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
---Open a font file and read its metadata.
|
||||
---@param font_path string
|
||||
---@return widget.fonts.info?
|
||||
---@return string|nil errmsg
|
||||
function FontInfo:read(font_path)
|
||||
self.data = {}
|
||||
self.path = font_path
|
||||
|
||||
local read, errmsg
|
||||
|
||||
---@diagnostic disable-next-line
|
||||
if type(renderer.font.get_metadata) == "function" then
|
||||
read, errmsg = fontinfo_read_native(self, font_path)
|
||||
else
|
||||
read, errmsg = fontinfo_read_nonnative(self, font_path)
|
||||
end
|
||||
|
||||
if not read then
|
||||
return read, errmsg
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
---Get the amount of collections on the font file.
|
||||
---@return integer
|
||||
function FontInfo:embedded_fonts_count()
|
||||
return #self.data
|
||||
end
|
||||
|
||||
---Get the metadata of a previously read font file without
|
||||
---copyright and license information which can be long.
|
||||
---@param idx? integer Optional position of the embedded font
|
||||
---@return widget.fonts.data?
|
||||
---@return string|nil errmsg
|
||||
function FontInfo:get_data(idx)
|
||||
idx = idx or 1
|
||||
local data = {}
|
||||
|
||||
if #self.data > 0 and self.data[idx] then
|
||||
data = self.data[idx]
|
||||
else
|
||||
return nil, self.last_error
|
||||
end
|
||||
|
||||
return {
|
||||
path = self.path,
|
||||
id = data.id,
|
||||
type = data.type,
|
||||
family = data.family,
|
||||
subfamily = data.subfamily,
|
||||
fullname = data.fullname,
|
||||
version = data.version,
|
||||
psname = data.psname,
|
||||
url = data.url,
|
||||
tfamily = data.tfamily,
|
||||
tsubfamily = data.tsubfamily,
|
||||
wwsfamily = data.wwsfamily,
|
||||
wwssubfamily = data.wwssubfamily,
|
||||
monospace = data.monospace or false
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
return FontInfo
|
||||
230
.config/lite-xl/libraries/widget/fonts/init.lua
Normal file
230
.config/lite-xl/libraries/widget/fonts/init.lua
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
local core = require "core"
|
||||
local common = require "core.common"
|
||||
local style = require "core.style"
|
||||
local FontCache = require "libraries.widget.fonts.cache"
|
||||
local StatusView = require "core.statusview"
|
||||
|
||||
---@class widget.fonts
|
||||
local Fonts = {}
|
||||
|
||||
---@type widget.fonts.cache | nil
|
||||
local fontcache = nil
|
||||
|
||||
---@type table<integer, string> | nil
|
||||
local fonts = nil
|
||||
|
||||
---Last time the status view item was rendered
|
||||
local last_statusview_render = 0
|
||||
|
||||
---The amount of fonts matching the user query
|
||||
local matching_fonts = 0
|
||||
|
||||
---Flag that indicates if command view font picker is for monospaced
|
||||
local pick_monospaced = false
|
||||
|
||||
---Generate the list of fonts displayed on the CommandView.
|
||||
---@param monospaced? boolean Only display fonts detected as monospaced.
|
||||
local function generate_fonts(monospaced)
|
||||
if fontcache then
|
||||
if fontcache.building then monospaced = false end
|
||||
fonts = {}
|
||||
for idx, f in ipairs(fontcache.fonts) do
|
||||
if not monospaced or (monospaced and f.monospace) then
|
||||
table.insert(fonts, f.fullname .. "||" .. idx)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Helper function to split a string by a given delimeter.
|
||||
local function split(s, delimeter, delimeter_pattern)
|
||||
if not delimeter_pattern then
|
||||
delimeter_pattern = delimeter
|
||||
end
|
||||
|
||||
local result = {};
|
||||
for match in (s..delimeter):gmatch("(.-)"..delimeter_pattern) do
|
||||
table.insert(result, match);
|
||||
end
|
||||
return result;
|
||||
end
|
||||
|
||||
local already_cleaning = false
|
||||
|
||||
---Clean the generated font cache used on command view to free some ram
|
||||
local function clean_fonts_cache()
|
||||
if not fontcache or already_cleaning then return end
|
||||
if not fontcache.building and not fontcache.searching_monospaced then
|
||||
fontcache = nil
|
||||
fonts = nil
|
||||
collectgarbage "collect"
|
||||
else
|
||||
already_cleaning = true
|
||||
core.add_thread(function()
|
||||
while fontcache.building or fontcache.searching_monospaced do
|
||||
coroutine.yield(1)
|
||||
end
|
||||
if
|
||||
core.active_view ~= core.command_view
|
||||
or
|
||||
(
|
||||
core.command_view.label ~= "Select Font: "
|
||||
and
|
||||
core.command_view.label ~= "List only monospaced fonts?: "
|
||||
)
|
||||
then
|
||||
fontcache = nil
|
||||
fonts = nil
|
||||
collectgarbage "collect"
|
||||
already_cleaning = false
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
---Launch the commandview and let the user select a font.
|
||||
---@param callback fun(name:string, path:string)
|
||||
---@param monospaced boolean
|
||||
function Fonts.show_picker(callback, monospaced)
|
||||
if not fontcache then fontcache = FontCache() end
|
||||
|
||||
pick_monospaced = monospaced
|
||||
|
||||
if not fontcache.building and (not monospaced or fontcache.monospaced) then
|
||||
generate_fonts(monospaced)
|
||||
else
|
||||
core.add_thread(function()
|
||||
while
|
||||
(fontcache.building or (monospaced and not fontcache.monospaced))
|
||||
and
|
||||
core.active_view == core.command_view
|
||||
and
|
||||
core.command_view.label == "Select Font: "
|
||||
do
|
||||
core.command_view:update_suggestions()
|
||||
coroutine.yield(2)
|
||||
end
|
||||
generate_fonts(monospaced)
|
||||
core.command_view:update_suggestions()
|
||||
end)
|
||||
end
|
||||
|
||||
last_statusview_render = system.get_time()
|
||||
|
||||
core.command_view:enter("Select Font", {
|
||||
submit = function(text, item)
|
||||
callback(item.text, item.info)
|
||||
clean_fonts_cache()
|
||||
end,
|
||||
suggest = function(text)
|
||||
if fontcache.building or (monospaced and fontcache.searching_monospaced) then
|
||||
generate_fonts(monospaced)
|
||||
end
|
||||
local res = common.fuzzy_match(fonts, text)
|
||||
matching_fonts = #res
|
||||
for i, name in ipairs(res) do
|
||||
local font_info = split(name, "||")
|
||||
local id = tonumber(font_info[2])
|
||||
local font_data = fontcache.fonts[id]
|
||||
res[i] = {
|
||||
text = font_data.fullname,
|
||||
info = font_data.path,
|
||||
id = id
|
||||
}
|
||||
end
|
||||
return res
|
||||
end,
|
||||
cancel = function()
|
||||
clean_fonts_cache()
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
---Same as `show_picker()` but asks the user if he wants a monospaced font.
|
||||
---@param callback fun(name:string, path:string)
|
||||
function Fonts.show_picker_ask_monospace(callback)
|
||||
if not fontcache then fontcache = FontCache() end
|
||||
|
||||
core.command_view:enter("List only monospaced fonts?", {
|
||||
submit = function(text, item)
|
||||
Fonts.show_picker(callback, item.mono)
|
||||
end,
|
||||
suggest = function(text)
|
||||
local res = common.fuzzy_match({"Yes", "No"}, text)
|
||||
for i, name in ipairs(res) do
|
||||
res[i] = {
|
||||
text = name,
|
||||
mono = text == "Yes" and true or false
|
||||
}
|
||||
end
|
||||
return res
|
||||
end,
|
||||
cancel = function()
|
||||
clean_fonts_cache()
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
---Check if the font cache is been built.
|
||||
---@return boolean building
|
||||
function Fonts.cache_is_building()
|
||||
if not fontcache then return false end
|
||||
return fontcache:is_building()
|
||||
end
|
||||
|
||||
---Remove current fonts cache file and regenerates a fresh one.
|
||||
---@return boolean started False if cache is already been built
|
||||
function Fonts.clean_cache()
|
||||
if not fontcache then fontcache = FontCache() end
|
||||
return fontcache:rebuild()
|
||||
end
|
||||
|
||||
core.status_view:add_item({
|
||||
predicate = function()
|
||||
return core.active_view == core.command_view
|
||||
and core.command_view.label == "Select Font: "
|
||||
end,
|
||||
name = "widget:font-select",
|
||||
alignment = StatusView.Item.LEFT,
|
||||
get_item = function()
|
||||
local found = 0
|
||||
local dots, status = "", ""
|
||||
if fontcache then
|
||||
if fontcache.building or fontcache.searching_monospaced then
|
||||
dots = "."
|
||||
if system.get_time() - last_statusview_render >= 3 then
|
||||
last_statusview_render = system.get_time()
|
||||
elseif system.get_time() - last_statusview_render >= 2 then
|
||||
dots = "..."
|
||||
elseif system.get_time() - last_statusview_render >= 1 then
|
||||
dots = ".."
|
||||
end
|
||||
end
|
||||
if fontcache.building then
|
||||
status = " | searching system fonts" .. dots
|
||||
elseif fontcache.searching_monospaced then
|
||||
status = " | detecting monospaced fonts" .. dots
|
||||
end
|
||||
|
||||
if fontcache.building or not pick_monospaced then
|
||||
found = fontcache.found
|
||||
else
|
||||
found = fontcache.found_monospaced
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
style.text,
|
||||
style.font,
|
||||
"Matches: "
|
||||
.. tostring(matching_fonts)
|
||||
.. " / "
|
||||
.. tostring(found)
|
||||
.. status
|
||||
}
|
||||
end,
|
||||
position = 1
|
||||
})
|
||||
|
||||
|
||||
return Fonts
|
||||
Loading…
Add table
Add a link
Reference in a new issue