From f24f81e674757c1072c3896ec97c67febb4dac46 Mon Sep 17 00:00:00 2001 From: zyj Date: Fri, 6 Mar 2026 11:59:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=89=80=E6=9C=89=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E5=AE=89=E8=A3=85=E5=AD=97=E4=BD=93=E7=9A=84=E6=96=B9?= =?UTF-8?q?=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/platform/darwin.go | 93 ++++++++++++++++++++++++++++++++ internal/platform/linux.go | 87 ++++++++++++++++++++++++++++++ internal/platform/windows.go | 102 ++++++++++++++++++++++++++++------- 3 files changed, 263 insertions(+), 19 deletions(-) diff --git a/internal/platform/darwin.go b/internal/platform/darwin.go index d0032fd..1f094c6 100644 --- a/internal/platform/darwin.go +++ b/internal/platform/darwin.go @@ -2,8 +2,13 @@ package platform import ( "context" + "fmt" + "io" "os" "os/exec" + "path/filepath" + + "github.com/user/devpack/internal/logging" ) // DarwinPlatform macOS 平台实现 @@ -64,6 +69,94 @@ func (p *DarwinPlatform) DefaultShell() string { return "/bin/zsh" } +// InstallFont 安装字体到用户级字体目录 +// macOS 会自动检测 ~/Library/Fonts/ 中的新字体,无需手动刷新缓存 +// 支持 .ttf (TrueType) 和 .otf (OpenType) 格式 func (p *DarwinPlatform) InstallFont(ctx context.Context, fontPath string) error { + log := logging.G() + + // 验证源字体文件是否存在且可访问 + info, err := os.Stat(fontPath) + if err != nil { + log.Errorf("无法访问字体文件: %v", err) + return fmt.Errorf("无法访问字体文件: %w", err) + } + if info.IsDir() { + return fmt.Errorf("字体路径是目录而非文件: %s", fontPath) + } + + // 检查字体格式是否受支持 + fontName := filepath.Base(fontPath) + ext := filepath.Ext(fontName) + if ext != ".ttf" && ext != ".otf" { + log.Errorf("不支持的字体格式: %s", ext) + return fmt.Errorf("不支持的字体格式: %s,仅支持 .ttf 和 .otf", ext) + } + + // 用户级字体目录: ~/Library/Fonts/ + // macOS 自动监视此目录,放入即可使用 + destDir := filepath.Join(p.homeDir, "Library", "Fonts") + log.Debugf("正在安装字体: %s 到 %s", fontPath, destDir) + + // 确保目标目录存在(通常已存在) + if err := os.MkdirAll(destDir, 0755); err != nil { + return fmt.Errorf("创建字体目录失败: %w", err) + } + + // 检查字体是否已安装 + destPath := filepath.Join(destDir, fontName) + if _, err := os.Stat(destPath); err == nil { + log.Infof("字体已存在,跳过安装: %s", destPath) + return nil + } + + // 复制字体文件到目标路径 + if err := darwinCopyFile(fontPath, destPath); err != nil { + return fmt.Errorf("复制字体文件失败: %w", err) + } + + // 可选:通过 atsutil 强制刷新字体服务缓存以立即生效 + // 某些应用可能需要重启才能看到新字体 + if _, err := exec.LookPath("atsutil"); err == nil { + // 移除用户字体数据库缓存 + cmd := exec.CommandContext(ctx, "atsutil", "databases", "-removeUser") + if out, err := cmd.CombinedOutput(); err != nil { + log.Debugf("清理字体缓存数据库: %s, %v", string(out), err) + } + // 重启字体服务 + cmd = exec.CommandContext(ctx, "atsutil", "server", "-shutdown") + if out, err := cmd.CombinedOutput(); err != nil { + log.Debugf("重启字体服务: %s, %v", string(out), err) + } + } + + log.Infof("字体安装成功: %s", fontName) + return nil +} + +// darwinCopyFile 复制文件,失败时自动清理目标文件 +func darwinCopyFile(src, dst string) error { + input, err := os.Open(src) + if err != nil { + return err + } + defer input.Close() + + output, err := os.Create(dst) + if err != nil { + return err + } + + _, err = io.Copy(output, input) + closeErr := output.Close() + + if err != nil { + os.Remove(dst) + return err + } + if closeErr != nil { + os.Remove(dst) + return closeErr + } return nil } diff --git a/internal/platform/linux.go b/internal/platform/linux.go index 180704a..002862b 100644 --- a/internal/platform/linux.go +++ b/internal/platform/linux.go @@ -2,8 +2,13 @@ package platform import ( "context" + "fmt" + "io" "os" "os/exec" + "path/filepath" + + "github.com/user/devpack/internal/logging" ) // LinuxPlatform Linux 平台实现 @@ -81,6 +86,88 @@ func (p *LinuxPlatform) DefaultShell() string { return "/bin/bash" } +// InstallFont 安装字体到用户级字体目录并刷新字体缓存 +// 支持 .ttf (TrueType) 和 .otf (OpenType) 格式 func (p *LinuxPlatform) InstallFont(ctx context.Context, fontPath string) error { + log := logging.G() + + // 验证源字体文件是否存在且可访问 + info, err := os.Stat(fontPath) + if err != nil { + log.Errorf("无法访问字体文件: %v", err) + return fmt.Errorf("无法访问字体文件: %w", err) + } + if info.IsDir() { + return fmt.Errorf("字体路径是目录而非文件: %s", fontPath) + } + + // 检查字体格式是否受支持 + fontName := filepath.Base(fontPath) + ext := filepath.Ext(fontName) + if ext != ".ttf" && ext != ".otf" { + log.Errorf("不支持的字体格式: %s", ext) + return fmt.Errorf("不支持的字体格式: %s,仅支持 .ttf 和 .otf", ext) + } + + // 用户级字体目录: ~/.local/share/fonts/ + destDir := filepath.Join(p.homeDir, ".local", "share", "fonts") + log.Debugf("正在安装字体: %s 到 %s", fontPath, destDir) + + // 确保目标目录存在 + if err := os.MkdirAll(destDir, 0755); err != nil { + return fmt.Errorf("创建字体目录失败: %w", err) + } + + // 检查字体是否已安装 + destPath := filepath.Join(destDir, fontName) + if _, err := os.Stat(destPath); err == nil { + log.Infof("字体已存在,跳过安装: %s", destPath) + return nil + } + + // 复制字体文件到目标路径 + if err := linuxCopyFile(fontPath, destPath); err != nil { + return fmt.Errorf("复制字体文件失败: %w", err) + } + + // 刷新字体缓存 + if _, err := exec.LookPath("fc-cache"); err == nil { + cmd := exec.CommandContext(ctx, "fc-cache", "-f", destDir) + if out, err := cmd.CombinedOutput(); err != nil { + log.Warnf("刷新字体缓存失败: %s, %v", string(out), err) + // 字体文件已复制成功,缓存刷新失败不是致命错误 + } + } else { + log.Warnf("未找到 fc-cache 命令,字体缓存未刷新,可能需要重新登录后生效") + } + + log.Infof("字体安装成功: %s", fontName) + return nil +} + +// linuxCopyFile 复制文件,失败时自动清理目标文件 +func linuxCopyFile(src, dst string) error { + input, err := os.Open(src) + if err != nil { + return err + } + defer input.Close() + + output, err := os.Create(dst) + if err != nil { + return err + } + + _, err = io.Copy(output, input) + closeErr := output.Close() + + if err != nil { + os.Remove(dst) + return err + } + if closeErr != nil { + os.Remove(dst) + return closeErr + } return nil } diff --git a/internal/platform/windows.go b/internal/platform/windows.go index 861aa0d..6ab09a6 100644 --- a/internal/platform/windows.go +++ b/internal/platform/windows.go @@ -2,11 +2,13 @@ package platform import ( "context" + "fmt" "io" "os" "os/exec" "path/filepath" + "github.com/user/devpack/internal/logging" "golang.org/x/sys/windows/registry" ) @@ -86,45 +88,107 @@ func (p *WindowsPlatform) DefaultShell() string { return "powershell" // Windows PowerShell 5.1 } -// InstallFont 安装字体 +// InstallFont 安装字体到用户级字体目录并注册到注册表 +// 支持 .ttf (TrueType) 和 .otf (OpenType) 格式 func (p *WindowsPlatform) InstallFont(ctx context.Context, fontPath string) error { - // 目标路径为 C:\Users\\AppData\Local\Microsoft\Windows\Fonts - // 也可以使用 C:\Users\\AppData\Roaming\Microsoft\Windows\Fonts,但 Local 更合适 + log := logging.G() + + // 验证源字体文件是否存在且可访问 + info, err := os.Stat(fontPath) + if err != nil { + log.Errorf("无法访问字体文件: %v", err) + return fmt.Errorf("无法访问字体文件: %w", err) + } + if info.IsDir() { + return fmt.Errorf("字体路径是目录而非文件: %s", fontPath) + } + + // 检查字体格式是否受支持 + fontName := filepath.Base(fontPath) + ext := filepath.Ext(fontName) + var regSuffix string + switch ext { + case ".ttf": + regSuffix = " (TrueType)" + case ".otf": + regSuffix = " (OpenType)" + default: + log.Errorf("不支持的字体格式: %s", ext) + return fmt.Errorf("不支持的字体格式: %s,仅支持 .ttf 和 .otf", ext) + } + + // 目标路径为用户级字体目录 + // C:\Users\\AppData\Local\Microsoft\Windows\Fonts destDir := filepath.Join(os.Getenv("LOCALAPPDATA"), "Microsoft", "Windows", "Fonts") + log.Debugf("正在安装字体: %s 到 %s", fontPath, destDir) // 确保目标目录存在 - os.MkdirAll(destDir, 0755) + if err := os.MkdirAll(destDir, 0755); err != nil { + return fmt.Errorf("创建字体目录失败: %w", err) + } - // 获取字体文件名 - fontName := filepath.Base(fontPath) - // 目标路径 + // 检查字体是否已安装 destPath := filepath.Join(destDir, fontName) + if _, err := os.Stat(destPath); err == nil { + log.Infof("字体已存在,跳过安装: %s", destPath) + return nil + } // 复制字体文件到目标路径 - input, err := os.Open(fontPath) + if err := copyFile(fontPath, destPath); err != nil { + return fmt.Errorf("复制字体文件失败: %w", err) + } + + // 写注册表以注册字体(用户级,无需管理员权限) + key, _, err := registry.CreateKey( + registry.CURRENT_USER, + `Software\Microsoft\Windows NT\CurrentVersion\Fonts`, + registry.ALL_ACCESS, + ) + if err != nil { + // 注册表写入失败时清理已复制的字体文件 + os.Remove(destPath) + return fmt.Errorf("打开字体注册表失败: %w", err) + } + defer key.Close() + + // 注册表键名:去掉扩展名的字体名 + 类型后缀 + baseName := fontName[:len(fontName)-len(ext)] + regName := baseName + regSuffix + + if err := key.SetStringValue(regName, destPath); err != nil { + os.Remove(destPath) + return fmt.Errorf("注册字体失败: %w", err) + } + + log.Infof("字体安装成功: %s", fontName) + return nil +} + +// copyFile 复制文件,失败时自动清理目标文件 +func copyFile(src, dst string) error { + input, err := os.Open(src) if err != nil { return err } defer input.Close() - output, err := os.Create(destPath) + output, err := os.Create(dst) if err != nil { return err } - defer output.Close() _, err = io.Copy(output, input) + // 先关闭文件确保数据写入磁盘 + closeErr := output.Close() + if err != nil { + os.Remove(dst) // 复制失败,清理残留文件 return err } - - // 写注册表以注册字体 - key, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Microsoft\Windows NT\CurrentVersion\Fonts`, registry.ALL_ACCESS) - if err != nil { - return err + if closeErr != nil { + os.Remove(dst) + return closeErr } - defer key.Close() - regName := fontName + " (TrueType)" - - return key.SetStringValue(regName, destPath) + return nil }