新增 egui 服务器管理面板并支持 SSH alias 多服务器巡检 接入硬件状态、服务状态、HTTP 探测和生产巡检状态展示 增加受控 systemd 启动关闭重启操作和中文字体注入 补充本地服务器面板技术方案与团队共享记忆
129 lines
3.6 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|