Files
Genarrative/server-rs/crates/server-manager-panel/src/fonts.rs
kdletters b54cbafc54 新增本地服务器管理面板
新增 egui 服务器管理面板并支持 SSH alias 多服务器巡检

接入硬件状态、服务状态、HTTP 探测和生产巡检状态展示

增加受控 systemd 启动关闭重启操作和中文字体注入

补充本地服务器面板技术方案与团队共享记忆
2026-06-11 22:33:05 +08:00

129 lines
3.6 KiB
Rust

use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Arc;
use eframe::egui::{FontData, FontDefinitions, FontFamily};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CjkFontCandidate {
pub path: PathBuf,
pub index: u32,
}
pub fn install_cjk_font(ctx: &eframe::egui::Context) -> Option<CjkFontCandidate> {
let candidate = find_cjk_font_candidate()?;
let bytes = std::fs::read(&candidate.path).ok()?;
let mut font_data = FontData::from_owned(bytes);
font_data.index = candidate.index;
let mut definitions = FontDefinitions::default();
definitions
.font_data
.insert("genarrative-cjk".to_owned(), Arc::new(font_data));
// 中文注释:作为 fallback 注入,保留 egui 默认拉丁/图标字体,同时补齐中文 glyph。
for family in [FontFamily::Proportional, FontFamily::Monospace] {
definitions
.families
.entry(family)
.or_default()
.push("genarrative-cjk".to_owned());
}
ctx.set_fonts(definitions);
Some(candidate)
}
pub fn find_cjk_font_candidate() -> Option<CjkFontCandidate> {
if let Ok(path) = std::env::var("GENARRATIVE_SERVER_PANEL_CJK_FONT") {
if let Some(candidate) = parse_font_spec(&path) {
return Some(candidate);
}
}
const KNOWN_PATHS: &[(&str, u32)] = &[
("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", 2),
("/usr/share/fonts/opentype/noto/NotoSansCJK-Medium.ttc", 2),
("/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", 0),
("/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", 0),
(
"/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf",
0,
),
(
"/home/dsk/.local/share/fonts/genarrative-cjk/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
0,
),
];
for (path, index) in KNOWN_PATHS {
if Path::new(path).is_file() {
return Some(CjkFontCandidate {
path: PathBuf::from(path),
index: *index,
});
}
}
for family in [
"Noto Sans CJK SC",
"WenQuanYi Zen Hei",
"Droid Sans Fallback",
] {
if let Some(candidate) = find_with_fc_match(family) {
return Some(candidate);
}
}
None
}
fn parse_font_spec(raw: &str) -> Option<CjkFontCandidate> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
let (path, index) = trimmed
.rsplit_once('|')
.and_then(|(path, index)| Some((path, index.parse().ok()?)))
.unwrap_or((trimmed, 0));
let path = PathBuf::from(path);
path.is_file().then_some(CjkFontCandidate { path, index })
}
fn find_with_fc_match(family: &str) -> Option<CjkFontCandidate> {
let output = Command::new("fc-match")
.arg("-f")
.arg("%{file}|%{index}\n")
.arg(family)
.output()
.ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout);
stdout.lines().find_map(parse_font_spec)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_font_path_with_index() {
let candidate = parse_font_spec("/tmp/missing-font.ttc|2");
assert_eq!(candidate, None);
}
#[test]
fn finds_existing_system_cjk_font() {
let candidate = find_cjk_font_candidate();
assert!(
candidate
.as_ref()
.is_some_and(|candidate| candidate.path.is_file()),
"expected at least one CJK font on this development host"
);
}
}