mirror of
https://github.com/alacritty/alacritty.git
synced 2024-11-25 14:05:41 -05:00
Refactor FontConfig wrappers
There's now a proper wrapper in place for working with the FontConfig library. This should help significantly with error handling with font loading; at least, the FontConfig code shouldn't panic. The FreeType rasterizer still needs to be updated to handle missing fonts, and a more sensible default font should be specified.
This commit is contained in:
parent
72ff775b23
commit
44c6171bc0
2 changed files with 394 additions and 112 deletions
25
font/Cargo.lock
generated
25
font/Cargo.lock
generated
|
@ -191,3 +191,28 @@ name = "winapi-build"
|
|||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[metadata]
|
||||
"checksum bitflags 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "72cd7314bd4ee024071241147222c706e80385a1605ac7d4cd2fcc339da2ae46"
|
||||
"checksum core-foundation 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "20a6d0448d3a99d977ae4a2aa5a98d886a923e863e81ad9ff814645b6feb3bbd"
|
||||
"checksum core-foundation-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "05eed248dc504a5391c63794fe4fb64f46f071280afaa1b73308f3c0ce4574c5"
|
||||
"checksum core-graphics 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0c56c6022ba22aedbaa7d231be545778becbe1c7aceda4c82ba2f2084dd4c723"
|
||||
"checksum core-text 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "94d4f3fab9e0242a648728764ac50e322b61eeb28c2d26d483721fe392cb2878"
|
||||
"checksum euclid 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)" = "c7b555729225fcc2aabc1ac951f9346967b35c901f4f03a480c31b6a45824109"
|
||||
"checksum expat-sys 2.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4ccf6f838594c1571f176f0afdbeb9cfa9f83b478f269d3f0390939b1df4323e"
|
||||
"checksum freetype-rs 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1e8c93a141b156862ab58d0206fa44a9b20d899c86c3e6260017ab748029aa42"
|
||||
"checksum freetype-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "eccfb6d96cac99921f0c2142a91765f6c219868a2c45bdfe7d65a08775f18127"
|
||||
"checksum gcc 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)" = "3da3a2cbaeb01363c8e3704fd9fd0eb2ceb17c6f27abd4c1ef040fb57d20dc79"
|
||||
"checksum heapsize 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "abb306abb8d398e053cfb1b3e7b72c2f580be048b85745c52652954f8ad1439c"
|
||||
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
|
||||
"checksum libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c96061f0c8a2dc27482e394d82e23073569de41d73cd736672ccd3e5c7471bfd"
|
||||
"checksum libz-sys 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c9795a8a0498b3abab873f8f063816fcc2e002388e89df87da065628dd5a8ed2"
|
||||
"checksum log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ab83497bf8bf4ed2a74259c1c802351fcd67a65baa86394b6ba73c36f4838054"
|
||||
"checksum make-cmd 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a8ca8afbe8af1785e09636acb5a41e08a765f5f0340568716c18a8700ba3c0d3"
|
||||
"checksum num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "51eab148f171aefad295f8cece636fc488b9b392ef544da31ea4b8ef6b9e9c39"
|
||||
"checksum pkg-config 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8cee804ecc7eaf201a4a207241472cc870e825206f6c031e3ee2a72fa425f2fa"
|
||||
"checksum rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)" = "6159e4e6e559c81bd706afe9c8fd68f547d3e851ce12e76b1de7914bab61691b"
|
||||
"checksum serde 0.7.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b76133a8a02f1c6ebd3fb9a2ecaab3d54302565a51320e80931adba571aadb1b"
|
||||
"checksum servo-fontconfig 0.2.0 (git+https://github.com/jwilm/rust-fontconfig)" = "<none>"
|
||||
"checksum servo-fontconfig-sys 2.11.3 (git+https://github.com/jwilm/libfontconfig)" = "<none>"
|
||||
"checksum winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "3969e500d618a5e974917ddefd0ba152e4bcaae5eb5d9b8c1fbc008e9e28c24e"
|
||||
"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
|
||||
|
|
|
@ -13,66 +13,360 @@
|
|||
// limitations under the License.
|
||||
//
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
mod fc {
|
||||
use std::ptr;
|
||||
use std::str::from_utf8;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::str;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use libc::{c_char, c_int};
|
||||
use fontconfig::fontconfig as ffi;
|
||||
|
||||
use fontconfig::fontconfig::{FcConfigGetCurrent, FcConfigGetFonts, FcSetSystem};
|
||||
use fontconfig::fontconfig::{FcPatternGetString, FcPatternCreate, FcPatternAddString};
|
||||
use fontconfig::fontconfig::{FcPatternGetInteger};
|
||||
use fontconfig::fontconfig::{FcObjectSetCreate, FcObjectSetAdd};
|
||||
use fontconfig::fontconfig::{FcResultMatch, FcFontSetList};
|
||||
use fontconfig::fontconfig::{FcChar8};
|
||||
use fontconfig::fontconfig::{FcFontSetDestroy, FcPatternDestroy, FcObjectSetDestroy};
|
||||
use self::ffi::{FcConfigGetCurrent, FcConfigGetFonts};
|
||||
use self::ffi::{FcPatternGetString, FcPatternCreate, FcPatternAddString};
|
||||
use self::ffi::{FcPatternGetInteger};
|
||||
use self::ffi::{FcObjectSetCreate, FcObjectSetAdd};
|
||||
use self::ffi::{FcResultMatch, FcFontSetList};
|
||||
use self::ffi::{FcChar8, FcConfig, FcPattern, FcFontSet, FcObjectSet};
|
||||
use self::ffi::{FcFontSetDestroy, FcPatternDestroy, FcObjectSetDestroy, FcConfigDestroy};
|
||||
|
||||
unsafe fn fc_char8_to_string(fc_str: *mut FcChar8) -> String {
|
||||
from_utf8(CStr::from_ptr(fc_str as *const c_char).to_bytes()).unwrap().to_owned()
|
||||
/// FcConfig - Font Configuration
|
||||
pub struct Config(*mut FcConfig);
|
||||
|
||||
/// FcFontSet
|
||||
pub struct FontSet(*mut FcFontSet);
|
||||
|
||||
/// FcFontSet reference
|
||||
pub struct FontSetRef(*mut FcFontSet);
|
||||
|
||||
/// Iterator over a font set
|
||||
pub struct FontSetIter<'a> {
|
||||
font_set: &'a FontSetRef,
|
||||
num_fonts: usize,
|
||||
current: usize,
|
||||
}
|
||||
|
||||
macro_rules! ref_type {
|
||||
($($owned:ty => $refty:ty),*) => {
|
||||
$(
|
||||
impl Deref for $owned {
|
||||
type Target = $refty;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
unsafe {
|
||||
&*(self.0 as *mut _)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for $owned {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
unsafe {
|
||||
&mut *(self.0 as *mut _)
|
||||
}
|
||||
}
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
/// FcPattern
|
||||
pub struct Pattern(*mut FcPattern);
|
||||
|
||||
/// FcObjectSet
|
||||
pub struct ObjectSet(*mut FcObjectSet);
|
||||
|
||||
/// FcObjectSet reference
|
||||
pub struct ObjectSetRef(*mut FcObjectSet);
|
||||
|
||||
ref_type! {
|
||||
ObjectSet => ObjectSetRef,
|
||||
Pattern => PatternRef,
|
||||
FontSet => FontSetRef
|
||||
}
|
||||
|
||||
impl Drop for ObjectSet {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
FcObjectSetDestroy(self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectSet {
|
||||
pub fn new() -> ObjectSet {
|
||||
ObjectSet(unsafe {
|
||||
FcObjectSetCreate()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl ObjectSetRef {
|
||||
fn add(&mut self, property: &[u8]) {
|
||||
unsafe {
|
||||
FcObjectSetAdd(self.0, property.as_ptr() as *mut c_char);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn add_file(&mut self) {
|
||||
self.add(b"file\0");
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn add_index(&mut self) {
|
||||
self.add(b"index\0");
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn add_style(&mut self) {
|
||||
self.add(b"style\0");
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! pattern_add_string {
|
||||
($name:ident => $object:expr) => {
|
||||
#[inline]
|
||||
pub fn $name(&mut self, value: &str) -> bool {
|
||||
unsafe {
|
||||
self.add_string($object, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
pub fn new() -> Pattern {
|
||||
Pattern(unsafe { FcPatternCreate() })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Pattern {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
FcPatternDestroy(self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// FcPattern reference
|
||||
pub struct PatternRef(*mut FcPattern);
|
||||
|
||||
/// Available font sets
|
||||
pub enum SetName {
|
||||
System = 0,
|
||||
Application = 1,
|
||||
}
|
||||
|
||||
pub unsafe fn char8_to_string(fc_str: *mut FcChar8) -> String {
|
||||
str::from_utf8(CStr::from_ptr(fc_str as *const c_char).to_bytes()).unwrap().to_owned()
|
||||
}
|
||||
|
||||
macro_rules! pattern_get_string {
|
||||
($($method:ident() => $property:expr),+) => {
|
||||
$(
|
||||
pub fn $method(&self, id: isize) -> Option<String> {
|
||||
unsafe {
|
||||
let mut format: *mut FcChar8 = ptr::null_mut();
|
||||
|
||||
let result = FcPatternGetString(
|
||||
self.0,
|
||||
$property.as_ptr() as *mut c_char,
|
||||
id as c_int,
|
||||
&mut format
|
||||
);
|
||||
|
||||
if result == FcResultMatch {
|
||||
Some(char8_to_string(format))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! pattern_get_integer {
|
||||
($($method:ident() => $property:expr),+) => {
|
||||
$(
|
||||
pub fn $method(&self, id: isize) -> Option<isize> {
|
||||
let mut index = 0 as c_int;
|
||||
unsafe {
|
||||
let result = FcPatternGetInteger(
|
||||
self.0,
|
||||
$property.as_ptr() as *mut c_char,
|
||||
id as c_int,
|
||||
&mut index
|
||||
);
|
||||
|
||||
if result == FcResultMatch {
|
||||
Some(index as isize)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
impl PatternRef {
|
||||
/// Add a string value to the pattern
|
||||
///
|
||||
/// If the returned value is `true`, the value is added at the end of
|
||||
/// any existing list, otherwise it is inserted at the beginning.
|
||||
///
|
||||
/// # Unsafety
|
||||
///
|
||||
/// `object` is not checked to be a valid null-terminated string
|
||||
unsafe fn add_string(&mut self, object: &[u8], value: &str) -> bool {
|
||||
let value = CString::new(&value[..]).unwrap();
|
||||
let value = value.as_ptr();
|
||||
|
||||
FcPatternAddString(
|
||||
self.0,
|
||||
object.as_ptr() as *mut c_char,
|
||||
value as *mut FcChar8
|
||||
) == 1
|
||||
}
|
||||
|
||||
pattern_add_string! {
|
||||
add_family => b"family\0"
|
||||
}
|
||||
|
||||
pattern_get_string! {
|
||||
fontformat() => b"fontformat\0",
|
||||
family() => b"family\0",
|
||||
file() => b"file\0",
|
||||
style() => b"style\0"
|
||||
}
|
||||
|
||||
pattern_get_integer! {
|
||||
index() => b"index\0"
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a FontSet {
|
||||
type Item = &'a PatternRef;
|
||||
type IntoIter = FontSetIter<'a>;
|
||||
fn into_iter(self) -> FontSetIter<'a> {
|
||||
let num_fonts = unsafe {
|
||||
(*self.0).nfont as isize
|
||||
};
|
||||
|
||||
FontSetIter {
|
||||
font_set: unsafe { &*(self.0 as *mut _) },
|
||||
num_fonts: num_fonts as _,
|
||||
current: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a FontSetRef {
|
||||
type Item = &'a PatternRef;
|
||||
type IntoIter = FontSetIter<'a>;
|
||||
fn into_iter(self) -> FontSetIter<'a> {
|
||||
let num_fonts = unsafe {
|
||||
(*self.0).nfont as isize
|
||||
};
|
||||
|
||||
FontSetIter {
|
||||
font_set: self,
|
||||
num_fonts: num_fonts as _,
|
||||
current: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for FontSetIter<'a> {
|
||||
type Item = &'a PatternRef;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current == self.num_fonts {
|
||||
None
|
||||
} else {
|
||||
let pattern = unsafe {
|
||||
let ptr = *(*self.font_set.0).fonts.offset(self.current as isize);
|
||||
&*(ptr as *mut _)
|
||||
};
|
||||
|
||||
self.current += 1;
|
||||
Some(pattern)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FontSet {
|
||||
pub fn list(
|
||||
config: &Config,
|
||||
source: &mut FontSetRef,
|
||||
pattern: &PatternRef,
|
||||
objects: &ObjectSetRef
|
||||
) -> FontSet {
|
||||
let raw = unsafe {
|
||||
FcFontSetList(
|
||||
config.0,
|
||||
&mut source.0,
|
||||
1 /* nsets */,
|
||||
pattern.0,
|
||||
objects.0
|
||||
)
|
||||
};
|
||||
FontSet(raw)
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Get the current configuration
|
||||
pub fn get_current() -> Config {
|
||||
Config(unsafe { FcConfigGetCurrent() })
|
||||
}
|
||||
|
||||
/// Returns one of the two sets of fonts from the configuration as
|
||||
/// specified by `set`.
|
||||
pub fn get_fonts<'a>(&'a self, set: SetName) -> &'a FontSetRef {
|
||||
unsafe {
|
||||
let ptr = FcConfigGetFonts(self.0, set as u32);
|
||||
&*(ptr as *mut _)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FontSet {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
FcFontSetDestroy(self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Config {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
if self.0 != FcConfigGetCurrent() {
|
||||
FcConfigDestroy(self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn list_families() -> Vec<String> {
|
||||
let mut families = Vec::new();
|
||||
unsafe {
|
||||
// https://www.freedesktop.org/software/fontconfig/fontconfig-devel/fcconfiggetcurrent.html
|
||||
let config = FcConfigGetCurrent(); // *mut FcConfig
|
||||
|
||||
// https://www.freedesktop.org/software/fontconfig/fontconfig-devel/fcconfiggetfonts.html
|
||||
let font_set = FcConfigGetFonts(config, FcSetSystem); // *mut FcFontSet
|
||||
|
||||
let nfont = (*font_set).nfont as isize;
|
||||
for i in 0..nfont {
|
||||
let font = (*font_set).fonts.offset(i); // *mut FcPattern
|
||||
let id = 0 as c_int;
|
||||
let mut family: *mut FcChar8 = ptr::null_mut();
|
||||
let mut format: *mut FcChar8 = ptr::null_mut();
|
||||
|
||||
let result = FcPatternGetString(*font,
|
||||
b"fontformat\0".as_ptr() as *mut c_char,
|
||||
id,
|
||||
&mut format);
|
||||
|
||||
if result != FcResultMatch {
|
||||
continue;
|
||||
let config = fc::Config::get_current();
|
||||
let font_set = config.get_fonts(fc::SetName::System);
|
||||
for font in font_set {
|
||||
if let Some(format) = font.fontformat(0) {
|
||||
if format == "TrueType" || format == "CFF" {
|
||||
let id = 0;
|
||||
while let Some(family) = font.family(id) {
|
||||
families.push(family);
|
||||
}
|
||||
|
||||
let format = fc_char8_to_string(format);
|
||||
|
||||
if format != "TrueType" && format != "CFF" {
|
||||
continue
|
||||
}
|
||||
|
||||
let mut id = 0;
|
||||
while FcPatternGetString(
|
||||
*font,
|
||||
b"family\0".as_ptr() as *mut c_char,
|
||||
id, &mut family
|
||||
) == FcResultMatch {
|
||||
let safe_family = fc_char8_to_string(family);
|
||||
id += 1;
|
||||
families.push(safe_family);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -125,71 +419,33 @@ impl Family {
|
|||
}
|
||||
}
|
||||
|
||||
static FILE: &'static [u8] = b"file\0";
|
||||
static FAMILY: &'static [u8] = b"family\0";
|
||||
static INDEX: &'static [u8] = b"index\0";
|
||||
static STYLE: &'static [u8] = b"style\0";
|
||||
|
||||
#[allow(mutable_transmutes)]
|
||||
pub fn get_family_info(family: String) -> Family {
|
||||
|
||||
let mut members = Vec::new();
|
||||
let config = fc::Config::get_current();
|
||||
let font_set = config.get_fonts(fc::SetName::System);
|
||||
|
||||
unsafe {
|
||||
let config = FcConfigGetCurrent(); // *mut FcConfig
|
||||
let mut font_set = FcConfigGetFonts(config, FcSetSystem); // *mut FcFontSet
|
||||
let mut pattern = fc::Pattern::new();
|
||||
pattern.add_family(&family);
|
||||
|
||||
let pattern = FcPatternCreate();
|
||||
let family_name = CString::new(&family[..]).unwrap();
|
||||
let family_name = family_name.as_ptr();
|
||||
|
||||
// Add family name to pattern. Use this for searching.
|
||||
FcPatternAddString(
|
||||
pattern,
|
||||
FAMILY.as_ptr() as *mut c_char,
|
||||
family_name as *mut FcChar8
|
||||
);
|
||||
|
||||
// Request filename, style, and index for each variant in family
|
||||
let object_set = FcObjectSetCreate(); // *mut FcObjectSet
|
||||
FcObjectSetAdd(object_set, FILE.as_ptr() as *mut c_char);
|
||||
FcObjectSetAdd(object_set, INDEX.as_ptr() as *mut c_char);
|
||||
FcObjectSetAdd(object_set, STYLE.as_ptr() as *mut c_char);
|
||||
|
||||
let variants = FcFontSetList(
|
||||
config,
|
||||
&mut font_set,
|
||||
1 /* nsets */,
|
||||
pattern, object_set
|
||||
);
|
||||
|
||||
let num_variant = (*variants).nfont as isize;
|
||||
|
||||
for i in 0..num_variant {
|
||||
let font = (*variants).fonts.offset(i);
|
||||
let mut file: *mut FcChar8 = ptr::null_mut();
|
||||
assert_eq!(FcPatternGetString(*font, FILE.as_ptr() as *mut c_char, 0, &mut file),
|
||||
FcResultMatch);
|
||||
let file = fc_char8_to_string(file);
|
||||
|
||||
let mut style: *mut FcChar8 = ptr::null_mut();
|
||||
assert_eq!(FcPatternGetString(*font, STYLE.as_ptr() as *mut c_char, 0, &mut style),
|
||||
FcResultMatch);
|
||||
let style = fc_char8_to_string(style);
|
||||
|
||||
let mut index = 0 as c_int;
|
||||
assert_eq!(FcPatternGetInteger(*font, INDEX.as_ptr() as *mut c_char, 0, &mut index),
|
||||
FcResultMatch);
|
||||
let mut objects = fc::ObjectSet::new();
|
||||
objects.add_file();
|
||||
objects.add_index();
|
||||
objects.add_style();
|
||||
|
||||
let variants = fc::FontSet::list(&config, unsafe { ::std::mem::transmute(font_set) }, &pattern, &objects);
|
||||
for variant in &variants {
|
||||
if let Some(file) = variant.file(0) {
|
||||
if let Some(style) = variant.style(0) {
|
||||
if let Some(index) = variant.index(0) {
|
||||
members.push(Variant {
|
||||
style: style,
|
||||
file: PathBuf::from(file),
|
||||
index: index as isize,
|
||||
});
|
||||
}
|
||||
|
||||
FcFontSetDestroy(variants);
|
||||
FcPatternDestroy(pattern);
|
||||
FcObjectSetDestroy(object_set);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Family {
|
||||
|
@ -199,7 +455,8 @@ pub fn get_family_info(family: String) -> Family {
|
|||
}
|
||||
|
||||
pub fn get_font_families() -> HashMap<String, Family> {
|
||||
list_families().into_iter()
|
||||
list_families()
|
||||
.into_iter()
|
||||
.map(|family| (family.clone(), get_family_info(family)))
|
||||
.collect()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue