use std::ffi::{CStr, CString};
use std::path::PathBuf;
use std::ptr;
use std::sync::OnceLock;
use fontconfig_sys as fc;
use fontconfig_sys::constants::{
FC_CHARSET, FC_FAMILY, FC_FILE, FC_INDEX, FC_LANG, FC_MONO, FC_SLANT,
FC_SLANT_ITALIC, FC_SPACING, FC_WEIGHT, FC_WEIGHT_BOLD,
};
struct FcHandle(*mut fc::FcConfig);
unsafe impl Send for FcHandle {}
unsafe impl Sync for FcHandle {}
static FC_HANDLE: OnceLock<FcHandle> = OnceLock::new();
fn fc_config() -> *mut fc::FcConfig {
FC_HANDLE
.get_or_init(|| FcHandle(unsafe { fc::FcConfigGetCurrent() }))
.0
}
pub fn discover_fallback(
primary_family: &str,
ch: char,
want_mono: bool,
want_bold: bool,
want_italic: bool,
) -> Option<(PathBuf, u32)> {
let cfg = fc_config();
if cfg.is_null() {
return None;
}
unsafe {
let charset = fc::FcCharSetCreate();
if charset.is_null() {
return None;
}
if fc::FcCharSetAddChar(charset, ch as fc::FcChar32) == 0 {
fc::FcCharSetDestroy(charset);
return None;
}
let pattern = fc::FcPatternCreate();
if pattern.is_null() {
fc::FcCharSetDestroy(charset);
return None;
}
let family_c = match CString::new(primary_family) {
Ok(s) => s,
Err(_) => CString::new("monospace").unwrap(),
};
fc::FcPatternAddString(
pattern,
FC_FAMILY.as_ptr(),
family_c.as_ptr() as *const fc::FcChar8,
);
fc::FcPatternAddCharSet(pattern, FC_CHARSET.as_ptr(), charset);
if let Some(lang) = current_lang() {
if let Ok(lang_c) = CString::new(lang) {
fc::FcPatternAddString(
pattern,
FC_LANG.as_ptr(),
lang_c.as_ptr() as *const fc::FcChar8,
);
}
}
if want_mono {
fc::FcPatternAddInteger(pattern, FC_SPACING.as_ptr(), FC_MONO);
}
if want_bold {
fc::FcPatternAddInteger(pattern, FC_WEIGHT.as_ptr(), FC_WEIGHT_BOLD);
}
if want_italic {
fc::FcPatternAddInteger(pattern, FC_SLANT.as_ptr(), FC_SLANT_ITALIC);
}
fc::FcConfigSubstitute(cfg, pattern, fc::FcMatchPattern);
fc::FcDefaultSubstitute(pattern);
let mut result: fc::FcResult = 0;
let font_set = fc::FcFontSort(
cfg,
pattern,
1, ptr::null_mut(),
&mut result,
);
let answer = if !font_set.is_null() && result == fc::FcResultMatch {
let set = &*font_set;
let mut found = None;
for i in 0..set.nfont as isize {
let candidate = *set.fonts.offset(i);
if pattern_has_char(candidate, ch) {
if let Some(pair) = pattern_path_and_index(candidate) {
found = Some(pair);
break;
}
}
}
found
} else {
None
};
if !font_set.is_null() {
fc::FcFontSetDestroy(font_set);
}
fc::FcPatternDestroy(pattern);
fc::FcCharSetDestroy(charset);
answer
}
}
unsafe fn pattern_has_char(pattern: *mut fc::FcPattern, ch: char) -> bool {
unsafe {
let mut charset_ptr: *mut fc::FcCharSet = ptr::null_mut();
let res =
fc::FcPatternGetCharSet(pattern, FC_CHARSET.as_ptr(), 0, &mut charset_ptr);
if res != fc::FcResultMatch || charset_ptr.is_null() {
return false;
}
fc::FcCharSetHasChar(charset_ptr, ch as fc::FcChar32) != 0
}
}
unsafe fn pattern_path_and_index(pattern: *mut fc::FcPattern) -> Option<(PathBuf, u32)> {
unsafe {
let mut file_ptr: *mut fc::FcChar8 = ptr::null_mut();
let res = fc::FcPatternGetString(pattern, FC_FILE.as_ptr(), 0, &mut file_ptr);
if res != fc::FcResultMatch || file_ptr.is_null() {
return None;
}
let path_str = CStr::from_ptr(file_ptr as *const std::ffi::c_char)
.to_str()
.ok()?
.to_string();
let mut index: i32 = 0;
let _ = fc::FcPatternGetInteger(pattern, FC_INDEX.as_ptr(), 0, &mut index);
Some((PathBuf::from(path_str), index.max(0) as u32))
}
}
fn current_lang() -> Option<String> {
let raw = std::env::var("LC_CTYPE")
.ok()
.or_else(|| std::env::var("LANG").ok())?;
if raw.is_empty() || raw == "C" || raw == "POSIX" {
return None;
}
let trimmed = raw
.split(['.', '@'])
.next()
.unwrap_or(&raw)
.replace('_', "-");
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
}