Compare commits
11 Commits
v0.0.1-tes
...
v0.7.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 13298931fd | |||
| 0a40258d9a | |||
| a9b7a69224 | |||
| 9acbc834a4 | |||
| 2ea749e0a8 | |||
| c0156a88d6 | |||
| 21e4710829 | |||
| 0b102dcb2a | |||
| d26558ad0a | |||
| c2c3b11d35 | |||
| 34a7e7d208 |
@@ -16,21 +16,23 @@ jobs:
|
||||
- name: Checkout
|
||||
run: |
|
||||
apk add git
|
||||
git clone -b dev https://hub.gaomia.site/titor/yoyo.git project
|
||||
cp -r project/* .
|
||||
cp -r project/.* . 2>/dev/null || true
|
||||
git clone https://hub.gaomia.site/titor/yoyo.git /workspace/titor/yoyo/project
|
||||
cp -r /workspace/titor/yoyo/project/* /workspace/titor/yoyo/
|
||||
cp -r /workspace/titor/yoyo/project/.* /workspace/titor/yoyo/ 2>/dev/null || true
|
||||
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cd /workspace/titor/yoyo
|
||||
chmod +x ./build.sh
|
||||
for p in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64; do
|
||||
os=${p%/*}
|
||||
arch=${p#*/}
|
||||
ext=""
|
||||
[ "$os" = "windows" ] && ext=".exe"
|
||||
GOOS=$os GOARCH=$arch go build -buildvcs=false -o "yoo-${os}-${arch}${ext}" ./cmd/yoyo
|
||||
./build.sh "$p" -o "yoo-${os}-${arch}${ext}"
|
||||
done
|
||||
|
||||
- name: Checksums
|
||||
@@ -40,17 +42,21 @@ jobs:
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.release_token }}
|
||||
run: |
|
||||
apk add curl
|
||||
|
||||
TAG_NAME="${GITHUB_REF#refs/tags/}"
|
||||
|
||||
# 创建 Release
|
||||
curl -s -X POST "https://hub.gaomia.site/api/v1/repos/titor/yoyo/releases" \
|
||||
# 创建 Release 并获取 release_id
|
||||
RELEASE_RESPONSE=$(curl -s -X POST "https://hub.gaomia.site/api/v1/repos/titor/yoyo/releases" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"${TAG_NAME}\",\"name\":\"${TAG_NAME}\",\"body\":\"Automated release\"}"
|
||||
-d "{\"tag_name\":\"${TAG_NAME}\",\"name\":\"${TAG_NAME}\",\"body\":\"Automated release\"}")
|
||||
|
||||
# 上传产物
|
||||
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
||||
|
||||
# 上传附件
|
||||
for f in yoo-* checksums.txt; do
|
||||
[ -f "$f" ] && curl -s -X POST "https://hub.gaomia.site/api/v1/repos/titor/yoyo/releases/upload?name=${TAG_NAME}" \
|
||||
[ -f "$f" ] && curl -s -X POST "https://hub.gaomia.site/api/v1/repos/titor/yoyo/releases/${RELEASE_ID}/assets" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-F "attachment=@$f"
|
||||
done
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,6 +5,7 @@ yoyo
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
yoo
|
||||
|
||||
# 测试
|
||||
*.test
|
||||
@@ -27,6 +28,7 @@ vendor/
|
||||
# 构建输出
|
||||
dist/
|
||||
build/
|
||||
project/
|
||||
|
||||
# Go工作区
|
||||
go.work
|
||||
@@ -34,4 +36,4 @@ go.work.sum
|
||||
|
||||
# 本地配置文件
|
||||
configs/local.yaml
|
||||
configs/*.local.yaml
|
||||
configs/*.local.yaml
|
||||
|
||||
94
build.sh
Executable file
94
build.sh
Executable file
@@ -0,0 +1,94 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||
|
||||
show_help() {
|
||||
echo "YOYO 编译脚本"
|
||||
echo ""
|
||||
echo "用法:"
|
||||
echo " ./build.sh 构建当前平台,输出 yoyo"
|
||||
echo " ./build.sh <平台> 交叉编译指定平台"
|
||||
echo " ./build.sh -h 显示帮助"
|
||||
echo ""
|
||||
echo "参数:"
|
||||
echo " -h 显示帮助信息"
|
||||
echo " -o <文件名> 指定输出文件名"
|
||||
echo ""
|
||||
echo "支持的平台:"
|
||||
echo " linux/amd64, linux/arm64"
|
||||
echo " darwin/amd64, darwin/arm64"
|
||||
echo " windows/amd64"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " ./build.sh # 构建当前平台"
|
||||
echo " ./build.sh linux/amd64 # 编译 Linux x64"
|
||||
echo " ./build.sh darwin/arm64 # 编译 macOS ARM"
|
||||
echo " ./build.sh windows/amd64 -o app.exe # 编译 Windows x64,自定义输出名"
|
||||
echo ""
|
||||
echo "输出文件名格式: yoo-<os>-<arch>[.exe]"
|
||||
echo " yoo-linux-amd64"
|
||||
echo " yoo-darwin-arm64"
|
||||
echo " yoo-windows-amd64.exe"
|
||||
}
|
||||
|
||||
OUTPUT_NAME="yoyo"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-h|--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
-o|--output)
|
||||
OUTPUT_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
linux/amd64)
|
||||
GOOS=linux GOARCH=amd64
|
||||
PLATFORM="linux-amd64"
|
||||
shift
|
||||
;;
|
||||
linux/arm64)
|
||||
GOOS=linux GOARCH=arm64
|
||||
PLATFORM="linux-arm64"
|
||||
shift
|
||||
;;
|
||||
darwin/amd64)
|
||||
GOOS=darwin GOARCH=amd64
|
||||
PLATFORM="darwin-amd64"
|
||||
shift
|
||||
;;
|
||||
darwin/arm64)
|
||||
GOOS=darwin GOARCH=arm64
|
||||
PLATFORM="darwin-arm64"
|
||||
shift
|
||||
;;
|
||||
windows/amd64)
|
||||
GOOS=windows GOARCH=amd64
|
||||
PLATFORM="windows-amd64"
|
||||
if [[ "$OUTPUT_NAME" == "yoyo" ]]; then
|
||||
OUTPUT_NAME="yoo-windows-amd64.exe"
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "未知平台: $1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "Building yoyo version: $VERSION"
|
||||
|
||||
if [ -n "$GOOS" ]; then
|
||||
echo "Target: $PLATFORM"
|
||||
go build -ldflags "-s -w -X github.com/titor/fanyi/internal/logo.version=${VERSION}" -o "$OUTPUT_NAME" ./cmd/yoyo
|
||||
else
|
||||
go build -ldflags "-s -w -X github.com/titor/fanyi/internal/logo.version=${VERSION}" -o "$OUTPUT_NAME" ./cmd/yoyo
|
||||
fi
|
||||
|
||||
echo "Build complete: ./$OUTPUT_NAME"
|
||||
96
changelog.md
96
changelog.md
@@ -70,6 +70,31 @@
|
||||
|
||||
## 版本历史
|
||||
|
||||
### 0.7.1 (2026-04-08) - TUI界面改进
|
||||
**类型**: 优化版本
|
||||
**状态**: 开发中
|
||||
|
||||
**改进内容**:
|
||||
- ✅ 添加帮助信息栏目 - 使用 bubbles help 组件,位于界面底部
|
||||
- ✅ 帮助按键绑定 - Ctrl+H 切换帮助显示
|
||||
- ✅ Logo 版本号注入 - 使用 build.sh + ldflags 自动注入 git 版本
|
||||
- ✅ 翻译卡片样式 - 翻译结果 Padding(1,3,1,3) 增加上方空隙
|
||||
- ✅ Viewport 上方内边距 - 第一个翻译卡片显示时增加上方空白
|
||||
|
||||
**构建脚本改进**:
|
||||
- ✅ 扩展 build.sh 支持跨平台编译
|
||||
- ✅ 添加 -h 帮助选项
|
||||
- ✅ 支持 -o 自定义输出文件名
|
||||
- ✅ 支持平台: linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64
|
||||
|
||||
**讨论记录**:
|
||||
- [帮助功能和样式改进](taolun.md#2026-04-08-tui界面帮助功能与样式改进)
|
||||
|
||||
**下一步**:
|
||||
- 实现模块8: 弹出框组件
|
||||
|
||||
---
|
||||
|
||||
### 0.7.0 (2026-04-06) - TUI界面改进
|
||||
**类型**: 功能版本
|
||||
**状态**: 开发中
|
||||
@@ -468,4 +493,73 @@ yoyo onboard --force
|
||||
### 版本号规则
|
||||
- 版本号需与 git 标签、changelog.md 中的版本号保持三方同步
|
||||
|
||||
**讨论记录**: [taolun.md#版本-100-beta-Logo和信息栏改造](taolun.md#版本-100-beta-logo和信息栏改造)
|
||||
**讨论记录**: [taolun.md#版本-100-beta-Logo和信息栏改造](taolun.md#版本-100-beta-logo和信息栏改造)
|
||||
|
||||
---
|
||||
|
||||
## v1.1.0 (2026-04-07)
|
||||
|
||||
### BUG修复
|
||||
- 修复配置文件路径使用相对路径导致管道模式下无法找到配置的问题
|
||||
- 修复onboard配置保存到错误路径的问题
|
||||
- 修复.env文件只在当前目录加载的问题
|
||||
|
||||
### 新功能
|
||||
- 配置文件路径智能解析:`~/.config/yoyo/config.yaml` → `./configs/config.yaml`
|
||||
- onboard配置向导迁移到 `charm.land/huh/v2`,替代 `survey` 库
|
||||
- 新增路径解析工具 `internal/config/path.go`
|
||||
|
||||
### 改进
|
||||
- 配置查找优先级:`--config` 参数 > `~/.config/yoyo/config.yaml` > `./configs/config.yaml`
|
||||
- 配置文件统一保存到 `~/.config/yoyo/config.yaml`(符合XDG规范)
|
||||
- .env文件统一从 `~/.config/yoyo/.env` 加载
|
||||
- onboard使用huh的Form+Group模式,更美观的交互体验
|
||||
- 移除 `github.com/AlecAivazis/survey/v2` 依赖
|
||||
|
||||
### 技术细节
|
||||
- 新增 `config.ResolveConfigPath()` 函数处理路径解析
|
||||
- 新增 `config.GetUserConfigPath()` 返回标准配置路径
|
||||
- 新增 `config.GetUserEnvPath()` 返回标准环境变量路径
|
||||
- 支持 `~` 路径展开
|
||||
- huh使用v2版本,支持泛型和链式API
|
||||
|
||||
**讨论记录**: [taolun.md#2026-04-07-配置路径修复和huh迁移](taolun.md)
|
||||
|
||||
---
|
||||
|
||||
## v1.1.1 (2026-04-08)
|
||||
|
||||
### BUG修复
|
||||
- 修复 `--help` `-h` `-?` `--version` 在默认交互式模式下无响应的问题
|
||||
- 原因:交互模式判断优先于help/version检查,导致flags被忽略
|
||||
|
||||
### 新功能
|
||||
- 新增 `internal/logo/logo.go` 模块,统一管理logo展示
|
||||
- 编译时通过 `-ldflags` 注入版本号,实现动态版本管理
|
||||
- 新增 `build.sh` 脚本,自动获取git版本并注入
|
||||
|
||||
### 改进
|
||||
- 帮助信息和版本输出使用渐变logo(紫→青色)
|
||||
- TUI头部与CLI帮助信息使用统一的logo模块
|
||||
- 移除TUI头部的 `[Ctrl+C 退出]` 显示
|
||||
- 统一版本号格式:` ( v1.1.1-dirty )` 或 ` ( )`(无版本时)
|
||||
|
||||
### 技术细节
|
||||
```go
|
||||
// logo模块核心函数
|
||||
func GradientText(text string, startColor, endColor string) string
|
||||
func GetLogoPattern() string // 返回4行ascii art
|
||||
func GetVersionSuffix() string // 返回 " (v1.1.1-dirty )" 或 " ( )"
|
||||
func PrintLogoWithVersion() // 打印完整logo
|
||||
```
|
||||
|
||||
```bash
|
||||
# build.sh 版本注入
|
||||
VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "")
|
||||
go build -ldflags "-X github.com/titor/fanyi/internal/logo.version=${VERSION}" -o yoyo ./cmd/yoyo
|
||||
```
|
||||
|
||||
- 渐变色方案:`#B413DC`(紫)→ `#00C8C8`(青)
|
||||
- TUI通过调用 `logo.GradientText(logo.GetLogoPattern(), "#B413DC", "#00C8C8")` 获取渐变logo
|
||||
|
||||
**讨论记录**: [taolun.md#2026-04-08-Logo模块化与渐变色统一](taolun.md)
|
||||
566
cmd/yoyo/main.go
Normal file
566
cmd/yoyo/main.go
Normal file
@@ -0,0 +1,566 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/titor/fanyi/internal/cache"
|
||||
"github.com/titor/fanyi/internal/config"
|
||||
"github.com/titor/fanyi/internal/lang"
|
||||
"github.com/titor/fanyi/internal/logo"
|
||||
"github.com/titor/fanyi/internal/onboard"
|
||||
"github.com/titor/fanyi/internal/provider"
|
||||
"github.com/titor/fanyi/internal/translator"
|
||||
"github.com/titor/fanyi/internal/tui"
|
||||
)
|
||||
|
||||
var (
|
||||
// 命令行参数
|
||||
version = flag.Bool("version", false, "显示版本信息")
|
||||
help = flag.Bool("help", false, "显示帮助信息")
|
||||
h = flag.Bool("h", false, "显示帮助信息")
|
||||
question = flag.Bool("?", false, "显示帮助信息")
|
||||
langFlag = flag.String("lang", "", "目标语言代码(如 zh-CN, en-US, cn, en 等)")
|
||||
langLong = flag.String("language", "", "目标语言代码(--lang的长格式)")
|
||||
configFile = flag.String("config", "", "配置文件路径")
|
||||
providerFlag = flag.String("provider", "", "指定翻译厂商")
|
||||
promptFlag = flag.String("prompt", "", "指定Prompt模式")
|
||||
onboardFlag = flag.Bool("onboard", false, "启动交互式配置向导")
|
||||
onboardForce = flag.Bool("onboard-force", false, "强制重新配置")
|
||||
quietFlag = flag.Bool("quiet", false, "静默模式,不显示统计信息")
|
||||
quietShort = flag.Bool("q", false, "静默模式,不显示统计信息(-q的短格式)")
|
||||
interactive = flag.Bool("interactive", false, "启动交互式翻译界面")
|
||||
interactiveShort = flag.Bool("i", false, "启动交互式翻译界面(-i的短格式)")
|
||||
)
|
||||
|
||||
// isPipeInput 检测是否有管道输入
|
||||
func isPipeInput() bool {
|
||||
fileInfo, err := os.Stdin.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// 检查是否是管道(字符设备且不是普通文件)
|
||||
return (fileInfo.Mode() & os.ModeCharDevice) == 0
|
||||
}
|
||||
|
||||
// readFromStdin 从标准输入读取所有内容
|
||||
func readFromStdin() (string, error) {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
var lines []string
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text())
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return "", fmt.Errorf("读取标准输入失败: %w", err)
|
||||
}
|
||||
return strings.Join(lines, "\n"), nil
|
||||
}
|
||||
|
||||
// isQuiet 检查是否处于静默模式
|
||||
func isQuiet() bool {
|
||||
return *quietFlag || *quietShort
|
||||
}
|
||||
|
||||
// isTTY 检查是否在TTY环境中
|
||||
func isTTY() bool {
|
||||
// 检查标准输出是否是TTY
|
||||
if fi, err := os.Stdout.Stat(); err == nil {
|
||||
if (fi.Mode() & os.ModeCharDevice) != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 检查标准错误是否是TTY
|
||||
if fi, err := os.Stderr.Stat(); err == nil {
|
||||
if (fi.Mode() & os.ModeCharDevice) != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 检查标准输入是否是TTY
|
||||
if fi, err := os.Stdin.Stat(); err == nil {
|
||||
if (fi.Mode() & os.ModeCharDevice) != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 解析命令行参数
|
||||
flag.Parse()
|
||||
|
||||
// 处理版本和帮助
|
||||
if *version {
|
||||
printVersion()
|
||||
return
|
||||
}
|
||||
|
||||
if *help || *h || *question {
|
||||
printHelp()
|
||||
return
|
||||
}
|
||||
|
||||
// 处理 -? 作为位置参数的情况
|
||||
if flag.NArg() > 0 && (flag.Arg(0) == "-?" || flag.Arg(0) == "?") {
|
||||
printHelp()
|
||||
return
|
||||
}
|
||||
|
||||
// 处理交互式模式
|
||||
if *interactive || *interactiveShort || shouldStartInteractive() {
|
||||
startInteractiveMode()
|
||||
return
|
||||
}
|
||||
|
||||
// 处理管道输入情况
|
||||
if isPipeInput() {
|
||||
// 管道模式下,没有参数则显示帮助
|
||||
if flag.NArg() == 0 {
|
||||
printHelp()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 获取第一个参数作为命令或文本
|
||||
firstArg := flag.Arg(0)
|
||||
|
||||
// 处理子命令
|
||||
if firstArg == "onboard" {
|
||||
force := false
|
||||
// 检查是否有--force参数
|
||||
for _, arg := range os.Args[1:] {
|
||||
if arg == "--force" || arg == "-f" {
|
||||
force = true
|
||||
break
|
||||
}
|
||||
}
|
||||
runOnboard(force)
|
||||
return
|
||||
}
|
||||
|
||||
// 处理缓存命令
|
||||
if firstArg == "cache" {
|
||||
if flag.NArg() < 2 {
|
||||
fmt.Fprintln(os.Stderr, "错误: cache命令需要子命令")
|
||||
fmt.Fprintln(os.Stderr, "可用子命令: clear, stats, cleanup")
|
||||
os.Exit(1)
|
||||
}
|
||||
cacheSubcommand := flag.Arg(1)
|
||||
runCacheCommand(cacheSubcommand)
|
||||
return
|
||||
}
|
||||
|
||||
// 创建上下文,支持Ctrl+C中断
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// 处理中断信号
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sigChan
|
||||
fmt.Println("\n收到中断信号,正在退出...")
|
||||
cancel()
|
||||
}()
|
||||
|
||||
// 获取要翻译的文本
|
||||
var text string
|
||||
var err error
|
||||
if isPipeInput() {
|
||||
// 从stdin读取
|
||||
text, err = readFromStdin()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "读取管道输入失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// 如果没有输入内容,显示错误
|
||||
if strings.TrimSpace(text) == "" {
|
||||
fmt.Fprintln(os.Stderr, "错误: 管道输入为空")
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
// 从命令行参数读取
|
||||
text = firstArg
|
||||
if text == "" {
|
||||
printHelp()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载环境变量文件
|
||||
_ = godotenv.Load(config.GetUserEnvPath())
|
||||
|
||||
// 加载配置
|
||||
configPath, err := config.ResolveConfigPath(*configFile)
|
||||
if err != nil {
|
||||
fmt.Printf("解析配置路径失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
configLoader := &config.YAMLConfigLoader{}
|
||||
cfg, err := configLoader.Load(configPath)
|
||||
if err != nil {
|
||||
fmt.Printf("加载配置失败: %v\n", err)
|
||||
fmt.Println("提示: 运行 'yoyo onboard' 进行配置")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 解析语言参数
|
||||
targetLang := parseLanguageFlag()
|
||||
if targetLang == "" {
|
||||
targetLang = cfg.DefaultTargetLang
|
||||
}
|
||||
|
||||
// 解析Prompt参数
|
||||
promptName := *promptFlag
|
||||
if promptName == "" {
|
||||
promptName = "simple"
|
||||
}
|
||||
|
||||
// 获取厂商配置
|
||||
providerName := *providerFlag
|
||||
if providerName == "" {
|
||||
providerName = cfg.DefaultProvider
|
||||
}
|
||||
|
||||
providerConfig, err := cfg.GetProviderConfig(providerName)
|
||||
if err != nil {
|
||||
fmt.Printf("获取厂商配置失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 创建厂商实例
|
||||
providerInstance, err := provider.CreateProvider(providerName, provider.ProviderConfig{
|
||||
APIHost: providerConfig.APIHost,
|
||||
APIKey: providerConfig.APIKey,
|
||||
Model: providerConfig.Model,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("创建厂商实例失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 创建翻译器
|
||||
translatorInstance := translator.NewTranslator(cfg, providerInstance)
|
||||
|
||||
// 设置翻译选项
|
||||
options := &translator.TranslateOptions{
|
||||
ToLang: targetLang,
|
||||
PromptName: promptName,
|
||||
}
|
||||
|
||||
// 执行翻译
|
||||
result, err := translatorInstance.Translate(ctx, text, options)
|
||||
if err != nil {
|
||||
fmt.Printf("翻译失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 输出结果
|
||||
fmt.Println(result.Translated)
|
||||
|
||||
// 根据quiet参数决定是否显示统计信息
|
||||
if !isQuiet() && result.Usage != nil {
|
||||
fmt.Fprintf(os.Stderr, "\n--- 用量统计 ---\n")
|
||||
fmt.Fprintf(os.Stderr, "模型: %s\n", result.Model)
|
||||
fmt.Fprintf(os.Stderr, "提示词: %d tokens\n", result.Usage.PromptTokens)
|
||||
fmt.Fprintf(os.Stderr, "完成: %d tokens\n", result.Usage.CompletionTokens)
|
||||
fmt.Fprintf(os.Stderr, "总计: %d tokens\n", result.Usage.TotalTokens)
|
||||
}
|
||||
}
|
||||
|
||||
// parseLanguageFlag 解析语言参数
|
||||
func parseLanguageFlag() string {
|
||||
// 优先使用--lang参数,然后是--language参数
|
||||
language := *langFlag
|
||||
if language == "" {
|
||||
language = *langLong
|
||||
}
|
||||
|
||||
// 如果没有指定语言参数,返回空字符串(将使用配置文件中的默认值)
|
||||
if language == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 使用语言解析模块处理语言代码
|
||||
return lang.ParseLanguageCode(language)
|
||||
}
|
||||
|
||||
// runOnboard 运行配置向导
|
||||
func runOnboard(force bool) {
|
||||
if err := onboard.RunOnboard(force); err != nil {
|
||||
fmt.Printf("配置向导失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// runCacheCommand 运行缓存命令
|
||||
func runCacheCommand(subcommand string) {
|
||||
// 加载环境变量文件
|
||||
_ = godotenv.Load(config.GetUserEnvPath())
|
||||
|
||||
// 加载配置
|
||||
configPath, err := config.ResolveConfigPath("")
|
||||
if err != nil {
|
||||
fmt.Printf("解析配置路径失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
configLoader := &config.YAMLConfigLoader{}
|
||||
cfg, err := configLoader.Load(configPath)
|
||||
if err != nil {
|
||||
fmt.Printf("加载配置失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 创建缓存实例
|
||||
if !cfg.Cache.Enabled {
|
||||
fmt.Fprintln(os.Stderr, "缓存功能未启用")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cacheConfig := &cache.CacheConfig{
|
||||
Enabled: cfg.Cache.Enabled,
|
||||
MaxRecords: cfg.Cache.MaxRecords,
|
||||
ExpireDays: cfg.Cache.ExpireDays,
|
||||
DBPath: cfg.Cache.DBPath,
|
||||
}
|
||||
cacheInstance, err := cache.NewSQLiteCache(cacheConfig)
|
||||
if err != nil {
|
||||
fmt.Printf("创建缓存实例失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer cacheInstance.Close()
|
||||
|
||||
cleanupManager := cache.NewCleanupManager(cacheInstance)
|
||||
ctx := context.Background()
|
||||
|
||||
switch subcommand {
|
||||
case "clear":
|
||||
if err := cleanupManager.ClearAll(ctx); err != nil {
|
||||
fmt.Printf("清空缓存失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("缓存已清空")
|
||||
case "stats":
|
||||
stats, err := cleanupManager.GetStats(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("获取缓存统计失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("缓存统计:\n")
|
||||
fmt.Printf(" 总记录数: %d\n", stats.TotalRecords)
|
||||
fmt.Printf(" 总大小: %.2f MB\n", float64(stats.TotalSizeBytes)/1024/1024)
|
||||
fmt.Printf(" 最早记录: %v\n", stats.OldestRecord)
|
||||
fmt.Printf(" 最新记录: %v\n", stats.NewestRecord)
|
||||
fmt.Printf(" 平均tokens/记录: %.2f\n", stats.AvgTokensPerRecord)
|
||||
case "cleanup":
|
||||
if err := cleanupManager.CleanupManual(ctx); err != nil {
|
||||
fmt.Printf("清理缓存失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("缓存清理完成")
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "未知的缓存子命令: %s\n", subcommand)
|
||||
fmt.Fprintln(os.Stderr, "可用子命令: clear, stats, cleanup")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// printVersion 显示版本信息
|
||||
func printVersion() {
|
||||
logo.PrintLogoWithVersion()
|
||||
}
|
||||
|
||||
// printHelp 显示帮助信息
|
||||
func printHelp() {
|
||||
logo.PrintLogoWithVersion()
|
||||
|
||||
fmt.Printf(`
|
||||
|
||||
使用方法:
|
||||
yoyo [选项] <文本>
|
||||
yoyo onboard [选项]
|
||||
|
||||
基本翻译:
|
||||
yoyo "Hello world" # 翻译为中文(默认)
|
||||
yoyo --lang=cn "Hello world" # 指定翻译为简体中文
|
||||
yoyo --lang=en "你好" # 翻译为英文
|
||||
yoyo --lang=zh-TW "Hello world" # 翻译为繁体中文
|
||||
|
||||
管道使用:
|
||||
cat file.txt | yoyo # 翻译文件内容
|
||||
cat file.txt | yoyo --lang=en # 翻译为英文
|
||||
cat file.txt | yoyo -q # 静默模式,只输出翻译结果
|
||||
echo "Hello" | yoyo --lang=cn # 翻译命令输出
|
||||
yoyo "Hello" | grep "你好" # 与其他命令组合使用
|
||||
|
||||
语言代码支持:
|
||||
- 标准格式: zh-CN, zh-TW, en-US, en-GB, ja, ko, es, fr, de 等
|
||||
- 简短别名: cn(中文), en(英文), jp(日文), kr(韩文) 等
|
||||
- 中文名称: chinese(中文), english(英文), japanese(日文) 等
|
||||
|
||||
命令:
|
||||
onboard 启动交互式配置向导
|
||||
onboard --force 强制重新配置
|
||||
cache clear 清空翻译缓存
|
||||
cache stats 查看缓存统计信息
|
||||
cache cleanup 清理过期缓存
|
||||
|
||||
翻译选项:
|
||||
--lang=<语言代码> 指定目标语言
|
||||
--language=<语言代码> 指定目标语言(长格式)
|
||||
--config=<路径> 指定配置文件路径
|
||||
--provider=<厂商> 指定翻译厂商
|
||||
--prompt=<模式> 指定Prompt模式
|
||||
--quiet, -q 静默模式,不显示统计信息
|
||||
--interactive, -i 启动交互式翻译界面
|
||||
|
||||
通用选项:
|
||||
-h, --help 显示帮助信息
|
||||
--version 显示版本信息
|
||||
|
||||
示例:
|
||||
yoyo "Hello world"
|
||||
yoyo --lang=cn "Hello world"
|
||||
yoyo --lang=en "你好世界"
|
||||
yoyo --lang=jp "Hello world"
|
||||
yoyo --lang=ko "Hello world"
|
||||
yoyo --provider=siliconflow --lang=cn "Hello"
|
||||
yoyo --prompt=technical --lang=zh-CN "API documentation"
|
||||
echo "Hello" | yoyo --lang=cn -q
|
||||
cat file.txt | yoyo --lang=en
|
||||
yoyo onboard
|
||||
yoyo onboard --force
|
||||
yoyo cache clear # 清空翻译缓存
|
||||
yoyo cache stats # 查看缓存统计信息
|
||||
yoyo cache cleanup # 清理过期缓存
|
||||
yoyo --interactive # 启动交互式翻译界面
|
||||
yoyo -i # 启动交互式翻译界面(短格式)
|
||||
|
||||
配置:
|
||||
- 配置文件: ~/.config/yoyo/config.yaml
|
||||
- 环境变量: ~/.config/yoyo/.env
|
||||
- 默认厂商: siliconflow
|
||||
- 默认目标语言: zh-CN (简体中文)
|
||||
|
||||
更多信息请访问: https://github.com/titor/fanyi
|
||||
`)
|
||||
}
|
||||
|
||||
// shouldStartInteractive 判断是否应该启动交互式模式
|
||||
func shouldStartInteractive() bool {
|
||||
// 如果没有参数,且不是管道输入,则启动交互模式
|
||||
if flag.NArg() == 0 && !isPipeInput() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// startInteractiveMode 启动交互式模式
|
||||
func startInteractiveMode() {
|
||||
// 检查是否在TTY环境中
|
||||
if !isTTY() {
|
||||
fmt.Fprintln(os.Stderr, "错误: 交互式模式需要在TTY环境中运行")
|
||||
fmt.Fprintln(os.Stderr, "请直接使用传统模式: yoyo \"要翻译的文本\"")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 加载环境变量文件
|
||||
_ = godotenv.Load(config.GetUserEnvPath())
|
||||
|
||||
// 加载配置
|
||||
configPath, err := config.ResolveConfigPath(*configFile)
|
||||
if err != nil {
|
||||
fmt.Printf("解析配置路径失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
configLoader := &config.YAMLConfigLoader{}
|
||||
cfg, err := configLoader.Load(configPath)
|
||||
if err != nil {
|
||||
fmt.Printf("加载配置失败: %v\n", err)
|
||||
fmt.Println("提示: 运行 'yoyo onboard' 进行配置")
|
||||
fmt.Println("提示: 使用默认配置启动交互模式...")
|
||||
|
||||
// 使用默认配置
|
||||
cfg = &config.Config{
|
||||
DefaultProvider: "siliconflow",
|
||||
DefaultModel: "gpt-3.5-turbo",
|
||||
Timeout: 30,
|
||||
DefaultTargetLang: "zh-CN",
|
||||
Providers: make(map[string]config.ProviderConfig),
|
||||
Prompts: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// 解析语言参数
|
||||
targetLang := parseLanguageFlag()
|
||||
if targetLang == "" {
|
||||
targetLang = cfg.DefaultTargetLang
|
||||
}
|
||||
|
||||
// 解析Prompt参数
|
||||
promptName := *promptFlag
|
||||
if promptName == "" {
|
||||
promptName = "simple"
|
||||
}
|
||||
|
||||
// 获取厂商配置
|
||||
providerName := *providerFlag
|
||||
if providerName == "" {
|
||||
providerName = cfg.DefaultProvider
|
||||
}
|
||||
|
||||
providerConfig, err := cfg.GetProviderConfig(providerName)
|
||||
if err != nil {
|
||||
fmt.Printf("获取厂商配置失败: %v\n", err)
|
||||
fmt.Println("使用默认厂商配置...")
|
||||
|
||||
// 使用默认配置
|
||||
providerConfig = config.ProviderConfig{
|
||||
APIHost: "https://api.siliconflow.cn/v1",
|
||||
APIKey: "",
|
||||
Model: "gpt-3.5-turbo",
|
||||
Enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
// 创建厂商实例
|
||||
providerInstance, err := provider.CreateProvider(providerName, provider.ProviderConfig{
|
||||
APIHost: providerConfig.APIHost,
|
||||
APIKey: providerConfig.APIKey,
|
||||
Model: providerConfig.Model,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("创建厂商实例失败: %v\n", err)
|
||||
fmt.Println("使用模拟翻译功能...")
|
||||
|
||||
// 这里可以创建一个模拟厂商实例
|
||||
// 暂时直接返回错误
|
||||
fmt.Println("交互式模式需要有效的厂商配置")
|
||||
return
|
||||
}
|
||||
|
||||
// 创建翻译器
|
||||
translatorInstance := translator.NewTranslator(cfg, providerInstance)
|
||||
|
||||
// 启动TUI界面
|
||||
fmt.Println("正在启动交互式翻译界面...")
|
||||
fmt.Println("使用 Ctrl+C 退出")
|
||||
|
||||
// 创建并运行TUI应用程序
|
||||
app := tui.NewApp(cfg, translatorInstance)
|
||||
if _, err := app.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "运行TUI界面失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
22
go.mod
22
go.mod
@@ -5,9 +5,8 @@ go 1.26.1
|
||||
require (
|
||||
charm.land/bubbles/v2 v2.1.0
|
||||
charm.land/bubbletea/v2 v2.0.2
|
||||
charm.land/huh/v2 v2.0.3
|
||||
charm.land/lipgloss/v2 v2.0.2
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/charmbracelet/bubbles v1.0.0
|
||||
github.com/go-enry/go-enry/v2 v2.9.5
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mattn/go-sqlite3 v1.14.37
|
||||
@@ -16,34 +15,25 @@ require (
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/bubbletea v1.3.10 // indirect
|
||||
github.com/catppuccin/go v0.2.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.2 // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-enry/go-oniguruma v1.2.1 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.2 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.21 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
)
|
||||
|
||||
85
go.sum
85
go.sum
@@ -2,84 +2,69 @@ charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g=
|
||||
charm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY=
|
||||
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
|
||||
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
|
||||
charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU=
|
||||
charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
|
||||
charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs=
|
||||
charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
|
||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
||||
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
|
||||
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
||||
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
|
||||
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
|
||||
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
|
||||
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
|
||||
github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
|
||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
|
||||
github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=
|
||||
github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
|
||||
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
||||
github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=
|
||||
github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=
|
||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
|
||||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/go-enry/go-enry/v2 v2.9.5 h1:HPhAQQHYwJgihL2PxBZiUMFWiROsGwOBdB6/D8zCUhY=
|
||||
github.com/go-enry/go-enry/v2 v2.9.5/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8=
|
||||
github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo=
|
||||
github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
|
||||
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
|
||||
github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
@@ -87,48 +72,18 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
||||
76
internal/config/path.go
Normal file
76
internal/config/path.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// ConfigDirName 配置目录名称
|
||||
ConfigDirName = "yoyo"
|
||||
// ConfigFileName 配置文件名
|
||||
ConfigFileName = "config.yaml"
|
||||
// EnvFileName 环境变量文件名
|
||||
EnvFileName = ".env"
|
||||
)
|
||||
|
||||
// ResolveConfigPath 解析配置文件路径
|
||||
// 优先级: 用户指定路径 > ~/.config/yoyo/config.yaml > ./configs/config.yaml
|
||||
func ResolveConfigPath(userPath string) (string, error) {
|
||||
// 1. 用户通过 --config 指定的路径
|
||||
if userPath != "" {
|
||||
return expandPath(userPath)
|
||||
}
|
||||
|
||||
// 2. 标准用户配置目录 ~/.config/yoyo/config.yaml
|
||||
userConfigPath := GetUserConfigPath()
|
||||
if _, err := os.Stat(userConfigPath); err == nil {
|
||||
return userConfigPath, nil
|
||||
}
|
||||
|
||||
// 3. 项目本地配置 ./configs/config.yaml(向后兼容)
|
||||
localConfigPath := "configs/config.yaml"
|
||||
if _, err := os.Stat(localConfigPath); err == nil {
|
||||
return localConfigPath, nil
|
||||
}
|
||||
|
||||
// 4. 都不存在,返回标准路径(onboard 会创建)
|
||||
return userConfigPath, nil
|
||||
}
|
||||
|
||||
// GetUserConfigDir 获取用户配置目录路径
|
||||
// 返回 ~/.config/yoyo
|
||||
func GetUserConfigDir() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
// 降级到当前目录
|
||||
return ".config/" + ConfigDirName
|
||||
}
|
||||
return filepath.Join(home, ".config", ConfigDirName)
|
||||
}
|
||||
|
||||
// GetUserConfigPath 获取用户配置文件路径
|
||||
// 返回 ~/.config/yoyo/config.yaml
|
||||
func GetUserConfigPath() string {
|
||||
return filepath.Join(GetUserConfigDir(), ConfigFileName)
|
||||
}
|
||||
|
||||
// GetUserEnvPath 获取用户环境变量文件路径
|
||||
// 返回 ~/.config/yoyo/.env
|
||||
func GetUserEnvPath() string {
|
||||
return filepath.Join(GetUserConfigDir(), EnvFileName)
|
||||
}
|
||||
|
||||
// expandPath 展开路径中的 ~ 符号
|
||||
func expandPath(path string) (string, error) {
|
||||
if strings.HasPrefix(path, "~") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("无法获取用户主目录: %w", err)
|
||||
}
|
||||
path = filepath.Join(home, path[1:])
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
79
internal/logo/logo.go
Normal file
79
internal/logo/logo.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package logo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var version string
|
||||
|
||||
func SetVersion(v string) { version = v }
|
||||
|
||||
func GetVersion() string { return version }
|
||||
|
||||
func GradientText(text string, startColor, endColor string) string {
|
||||
startR, startG, startB := parseHexColor(startColor)
|
||||
endR, endG, endB := parseHexColor(endColor)
|
||||
|
||||
lines := strings.Split(text, "\n")
|
||||
if len(lines) == 0 {
|
||||
return text
|
||||
}
|
||||
|
||||
var result []string
|
||||
for _, line := range lines {
|
||||
if len(line) == 0 {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
|
||||
var coloredLine string
|
||||
for i, char := range line {
|
||||
ratio := float64(i) / float64(len(line)-1)
|
||||
r := int(float64(startR) + float64(endR-startR)*ratio)
|
||||
g := int(float64(startG) + float64(endG-startG)*ratio)
|
||||
b := int(float64(startB) + float64(endB-startB)*ratio)
|
||||
coloredLine += fmt.Sprintf("\033[38;2;%d;%d;%dm%s\033[0m", r, g, b, string(char))
|
||||
}
|
||||
result = append(result, coloredLine)
|
||||
}
|
||||
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
func parseHexColor(hex string) (int, int, int) {
|
||||
hex = strings.TrimPrefix(hex, "#")
|
||||
if len(hex) != 6 {
|
||||
return 0, 0, 0
|
||||
}
|
||||
var r, g, b int
|
||||
fmt.Sscanf(hex, "%02x%02x%02x", &r, &g, &b)
|
||||
return r, g, b
|
||||
}
|
||||
|
||||
func GetLogoPattern() string {
|
||||
return " _ _ _____ _____ " + "\n" +
|
||||
"( \\/ ( _ ( _ ) " + "\n" +
|
||||
" \\ / )(_)( )(_)( " + "\n" +
|
||||
" (__)(_____(_____("
|
||||
}
|
||||
|
||||
func GetVersionSuffix() string {
|
||||
v := GetVersion()
|
||||
if v != "" {
|
||||
return " (" + v + " )"
|
||||
}
|
||||
return " ( )"
|
||||
}
|
||||
|
||||
func PrintLogoWithVersion() {
|
||||
logoPattern := " _ _ _____ _____\n" +
|
||||
"( \\/ ( _ ( _ )\n" +
|
||||
" \\ / )(_)( )(_)( \n" +
|
||||
" (__)(_____(_____("
|
||||
|
||||
patternWithVersion := logoPattern + GetVersionSuffix()
|
||||
|
||||
colored := GradientText(patternWithVersion, "#B413DC", "#00C8C8")
|
||||
fmt.Println(colored)
|
||||
}
|
||||
@@ -1,30 +1,39 @@
|
||||
package onboard
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"charm.land/huh/v2"
|
||||
"github.com/titor/fanyi/internal/config"
|
||||
"github.com/titor/fanyi/internal/lang"
|
||||
)
|
||||
|
||||
// RunOnboard 启动配置向导
|
||||
func RunOnboard(force bool) error {
|
||||
fmt.Println("欢迎使用YOYO翻译工具配置向导!")
|
||||
fmt.Println("这个向导将帮助您配置翻译工具。")
|
||||
fmt.Println()
|
||||
configPath := config.GetUserConfigPath()
|
||||
|
||||
// 检查配置文件是否存在
|
||||
configPath := "configs/config.yaml"
|
||||
if _, err := os.Stat(configPath); err == nil && !force {
|
||||
overwrite := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: "检测到配置文件已存在,是否要重新配置?",
|
||||
Default: false,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &overwrite); err != nil {
|
||||
var overwrite bool
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewConfirm().
|
||||
Title("检测到配置文件已存在,是否要重新配置?").
|
||||
Affirmative("是").
|
||||
Negative("否").
|
||||
Value(&overwrite),
|
||||
),
|
||||
)
|
||||
if err := form.Run(); err != nil {
|
||||
if errors.Is(err, huh.ErrUserAborted) {
|
||||
fmt.Println("\n你已取消本次配置")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("用户输入错误: %w", err)
|
||||
}
|
||||
if !overwrite {
|
||||
@@ -34,30 +43,75 @@ func RunOnboard(force bool) error {
|
||||
}
|
||||
|
||||
// 步骤1: 选择主要厂商
|
||||
fmt.Println("步骤1: 选择主要翻译服务提供商")
|
||||
providerName, err := SelectProvider()
|
||||
if err != nil {
|
||||
var providerName string
|
||||
providerForm := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[string]().
|
||||
Title("请选择要使用的翻译服务提供商").
|
||||
Options(
|
||||
huh.NewOption("硅基流动 (推荐,免费额度)", "siliconflow"),
|
||||
huh.NewOption("火山引擎", "volcano"),
|
||||
huh.NewOption("国家超算", "national"),
|
||||
huh.NewOption("Qwen (通义千问)", "qwen"),
|
||||
huh.NewOption("OpenAI兼容格式", "openai"),
|
||||
).
|
||||
Value(&providerName),
|
||||
),
|
||||
)
|
||||
if err := providerForm.Run(); err != nil {
|
||||
if errors.Is(err, huh.ErrUserAborted) {
|
||||
fmt.Println("\n你已取消本次配置")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("选择厂商失败: %w", err)
|
||||
}
|
||||
|
||||
// 步骤2: 配置主要厂商
|
||||
fmt.Println("\n步骤2: 配置主要厂商")
|
||||
providerConfig, err := ConfigureProvider(providerName)
|
||||
providerConfig, err := ConfigureProviderHuh(providerName)
|
||||
if err != nil {
|
||||
if errors.Is(err, huh.ErrUserAborted) {
|
||||
fmt.Println("\n你已取消本次配置")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("配置厂商失败: %w", err)
|
||||
}
|
||||
|
||||
// 步骤3: 全局设置
|
||||
fmt.Println("\n步骤3: 全局设置")
|
||||
globalConfig, err := GlobalSettings()
|
||||
globalConfig, err := GlobalSettingsHuh()
|
||||
if err != nil {
|
||||
if errors.Is(err, huh.ErrUserAborted) {
|
||||
fmt.Println("\n你已取消本次配置")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("全局设置失败: %w", err)
|
||||
}
|
||||
|
||||
// 步骤4: 确认并保存配置
|
||||
fmt.Println("\n步骤4: 保存配置")
|
||||
configData := BuildConfig(providerName, providerConfig, globalConfig)
|
||||
|
||||
var confirmSave bool
|
||||
confirmForm := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewConfirm().
|
||||
Title("确认保存配置?").
|
||||
Description(fmt.Sprintf("配置文件将保存到: %s", configPath)).
|
||||
Affirmative("是,保存").
|
||||
Negative("否,取消").
|
||||
Value(&confirmSave),
|
||||
),
|
||||
)
|
||||
if err := confirmForm.Run(); err != nil {
|
||||
if errors.Is(err, huh.ErrUserAborted) {
|
||||
fmt.Println("\n你已取消本次配置")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("用户输入错误: %w", err)
|
||||
}
|
||||
if !confirmSave {
|
||||
fmt.Println("配置已取消。")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := SaveConfig(configData, configPath); err != nil {
|
||||
return fmt.Errorf("保存配置失败: %w", err)
|
||||
}
|
||||
@@ -71,54 +125,17 @@ func RunOnboard(force bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SelectProvider 选择主要厂商
|
||||
func SelectProvider() (string, error) {
|
||||
providers := []string{
|
||||
"siliconflow",
|
||||
"volcano",
|
||||
"national",
|
||||
"qwen",
|
||||
"openai",
|
||||
}
|
||||
|
||||
providerNames := map[string]string{
|
||||
"siliconflow": "硅基流动 (推荐,免费额度)",
|
||||
"volcano": "火山引擎",
|
||||
"national": "国家超算",
|
||||
"qwen": "Qwen (通义千问)",
|
||||
"openai": "OpenAI兼容格式",
|
||||
}
|
||||
|
||||
var selected string
|
||||
prompt := &survey.Select{
|
||||
Message: "请选择要使用的翻译服务提供商:",
|
||||
Options: func() []string {
|
||||
var opts []string
|
||||
for _, p := range providers {
|
||||
opts = append(opts, providerNames[p])
|
||||
}
|
||||
return opts
|
||||
}(),
|
||||
Default: providerNames["siliconflow"],
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &selected); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 返回对应的厂商名称
|
||||
for name, displayName := range providerNames {
|
||||
if displayName == selected {
|
||||
return name, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "siliconflow", nil
|
||||
// GlobalConfig 全局设置配置
|
||||
type GlobalConfig struct {
|
||||
DefaultProvider string
|
||||
DefaultModel string
|
||||
Timeout int
|
||||
DefaultSourceLang string
|
||||
DefaultTargetLang string
|
||||
}
|
||||
|
||||
// ConfigureProvider 配置厂商
|
||||
func ConfigureProvider(providerName string) (config.ProviderConfig, error) {
|
||||
// 厂商默认配置
|
||||
// ConfigureProviderHuh 使用 huh 配置厂商
|
||||
func ConfigureProviderHuh(providerName string) (config.ProviderConfig, error) {
|
||||
defaults := map[string]config.ProviderConfig{
|
||||
"siliconflow": {
|
||||
APIHost: "https://api.siliconflow.cn/v1",
|
||||
@@ -154,47 +171,43 @@ func ConfigureProvider(providerName string) (config.ProviderConfig, error) {
|
||||
Enabled: defaultConfig.Enabled,
|
||||
}
|
||||
|
||||
// 输入API密钥
|
||||
apiKeyPrompt := &survey.Input{
|
||||
Message: fmt.Sprintf("请输入 %s 的API密钥:", providerName),
|
||||
Help: "API密钥用于身份验证,将存储在配置文件中",
|
||||
}
|
||||
if err := survey.AskOne(apiKeyPrompt, &cfg.APIKey, survey.WithValidator(survey.Required)); err != nil {
|
||||
return config.ProviderConfig{}, err
|
||||
}
|
||||
|
||||
// 确认API HOST
|
||||
apiHostPrompt := &survey.Input{
|
||||
Message: "API HOST (直接回车使用默认值):",
|
||||
Default: cfg.APIHost,
|
||||
}
|
||||
if err := survey.AskOne(apiHostPrompt, &cfg.APIHost); err != nil {
|
||||
return config.ProviderConfig{}, err
|
||||
}
|
||||
|
||||
// 确认默认模型
|
||||
modelPrompt := &survey.Input{
|
||||
Message: "默认模型 (直接回车使用默认值):",
|
||||
Default: cfg.Model,
|
||||
}
|
||||
if err := survey.AskOne(modelPrompt, &cfg.Model); err != nil {
|
||||
var apiKey string
|
||||
apiKeyForm := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title(fmt.Sprintf("请输入 %s 的API密钥", providerName)).
|
||||
Description("API密钥用于身份验证,将存储在配置文件中").
|
||||
Value(&apiKey).
|
||||
Validate(func(str string) error {
|
||||
if strings.TrimSpace(str) == "" {
|
||||
return fmt.Errorf("API密钥不能为空")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
|
||||
huh.NewInput().
|
||||
Title("API HOST").
|
||||
Description("直接回车使用默认值").
|
||||
Value(&cfg.APIHost).
|
||||
Placeholder(defaultConfig.APIHost),
|
||||
|
||||
huh.NewInput().
|
||||
Title("默认模型").
|
||||
Description("直接回车使用默认值").
|
||||
Value(&cfg.Model).
|
||||
Placeholder(defaultConfig.Model),
|
||||
),
|
||||
)
|
||||
if err := apiKeyForm.Run(); err != nil {
|
||||
return config.ProviderConfig{}, err
|
||||
}
|
||||
|
||||
cfg.APIKey = apiKey
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// GlobalSettings 全局设置
|
||||
type GlobalConfig struct {
|
||||
DefaultProvider string
|
||||
DefaultModel string
|
||||
Timeout int
|
||||
DefaultSourceLang string
|
||||
DefaultTargetLang string
|
||||
}
|
||||
|
||||
// GlobalSettings 全局设置
|
||||
func GlobalSettings() (*GlobalConfig, error) {
|
||||
// GlobalSettingsHuh 使用 huh 进行全局设置
|
||||
func GlobalSettingsHuh() (*GlobalConfig, error) {
|
||||
cfg := &GlobalConfig{
|
||||
DefaultProvider: "siliconflow",
|
||||
DefaultModel: "siliconflow-base",
|
||||
@@ -203,43 +216,33 @@ func GlobalSettings() (*GlobalConfig, error) {
|
||||
DefaultTargetLang: "zh-CN",
|
||||
}
|
||||
|
||||
// 选择默认语言
|
||||
targetLangOptions := lang.GetCommonLanguages()
|
||||
var targetLangDisplay []string
|
||||
var options []huh.Option[string]
|
||||
for _, code := range targetLangOptions {
|
||||
targetLangDisplay = append(targetLangDisplay, fmt.Sprintf("%s (%s)", code, lang.GetLanguageName(code)))
|
||||
options = append(options, huh.NewOption(
|
||||
fmt.Sprintf("%s (%s)", code, lang.GetLanguageName(code)),
|
||||
code,
|
||||
))
|
||||
}
|
||||
|
||||
targetLangPrompt := &survey.Select{
|
||||
Message: "请选择默认目标语言:",
|
||||
Options: targetLangDisplay,
|
||||
Default: fmt.Sprintf("%s (%s)", "zh-CN", lang.GetLanguageName("zh-CN")),
|
||||
}
|
||||
|
||||
var selectedTarget string
|
||||
if err := survey.AskOne(targetLangPrompt, &selectedTarget); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 从选择中提取语言代码
|
||||
for i, display := range targetLangDisplay {
|
||||
if display == selectedTarget {
|
||||
cfg.DefaultTargetLang = targetLangOptions[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 设置超时时间
|
||||
timeoutPrompt := &survey.Input{
|
||||
Message: "API超时时间(秒):",
|
||||
Default: fmt.Sprintf("%d", cfg.Timeout),
|
||||
}
|
||||
var timeoutStr string
|
||||
if err := survey.AskOne(timeoutPrompt, &timeoutStr); err != nil {
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[string]().
|
||||
Title("请选择默认目标语言").
|
||||
Options(options...).
|
||||
Value(&cfg.DefaultTargetLang),
|
||||
|
||||
huh.NewInput().
|
||||
Title("API超时时间(秒)").
|
||||
Value(&timeoutStr).
|
||||
Placeholder("30"),
|
||||
),
|
||||
)
|
||||
if err := form.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析超时时间
|
||||
if timeout := parseIntOrDefault(timeoutStr, 30); timeout > 0 {
|
||||
cfg.Timeout = timeout
|
||||
}
|
||||
@@ -249,12 +252,10 @@ func GlobalSettings() (*GlobalConfig, error) {
|
||||
|
||||
// BuildConfig 构建配置对象
|
||||
func BuildConfig(providerName string, providerConfig config.ProviderConfig, globalConfig *GlobalConfig) *config.Config {
|
||||
// 创建厂商配置
|
||||
providers := map[string]config.ProviderConfig{
|
||||
providerName: providerConfig,
|
||||
}
|
||||
|
||||
// 创建Prompt配置
|
||||
prompts := map[string]string{
|
||||
"technical": "你是一位专业的技术翻译,请准确翻译以下技术文档,保持专业术语的准确性。",
|
||||
"creative": "你是一位富有创造力的翻译家,请用优美流畅的语言翻译以下内容。",
|
||||
@@ -275,25 +276,24 @@ func BuildConfig(providerName string, providerConfig config.ProviderConfig, glob
|
||||
|
||||
// SaveConfig 保存配置文件
|
||||
func SaveConfig(cfg *config.Config, path string) error {
|
||||
// 确保目录存在
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("创建配置目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 使用config包的Save方法
|
||||
loader := &config.YAMLConfigLoader{}
|
||||
return loader.Save(cfg, path)
|
||||
}
|
||||
|
||||
// parseIntOrDefault 解析整数,失败时返回默认值
|
||||
func parseIntOrDefault(s string, defaultValue int) int {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
var result int
|
||||
if _, err := fmt.Sscanf(s, "%d", &result); err != nil {
|
||||
result, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ type KeyMap struct {
|
||||
ScrollDown key.Binding
|
||||
ScrollTop key.Binding
|
||||
ScrollBottom key.Binding
|
||||
Help key.Binding
|
||||
}
|
||||
|
||||
func NewKeyMap() KeyMap {
|
||||
@@ -44,5 +45,21 @@ func NewKeyMap() KeyMap {
|
||||
key.WithKeys("end"),
|
||||
key.WithHelp("End", "底部"),
|
||||
),
|
||||
Help: key.NewBinding(
|
||||
key.WithKeys("ctrl+h"),
|
||||
key.WithHelp("Ctrl+H", "帮助"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (k KeyMap) ShortHelp() []key.Binding {
|
||||
return []key.Binding{k.Help, k.Quit}
|
||||
}
|
||||
|
||||
func (k KeyMap) FullHelp() [][]key.Binding {
|
||||
return [][]key.Binding{
|
||||
{k.Quit, k.Clear, k.SwitchLang},
|
||||
{k.ScrollUp, k.ScrollDown, k.ScrollTop, k.ScrollBottom},
|
||||
{k.Help},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,23 +7,20 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"charm.land/bubbles/v2/help"
|
||||
"charm.land/bubbles/v2/key"
|
||||
"charm.land/bubbles/v2/spinner"
|
||||
"charm.land/bubbles/v2/textarea"
|
||||
"charm.land/bubbles/v2/viewport"
|
||||
"charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/titor/fanyi/internal/config"
|
||||
"github.com/titor/fanyi/internal/content"
|
||||
"github.com/titor/fanyi/internal/logo"
|
||||
"github.com/titor/fanyi/internal/translator"
|
||||
)
|
||||
|
||||
var supportedLangs = []string{"zh-CN", "en-US", "ja", "ko", "zh-TW", "es", "fr", "de"}
|
||||
|
||||
var logoPattern = "l_ _ _____ _____ " + "\n" +
|
||||
"( \\/ ( _ ( _ ) " + "\n" +
|
||||
" \\ / )(_)( )(_)( " + "\n" +
|
||||
" (__)(_____(_____((() [v" + content.Version + "]"
|
||||
|
||||
type translateMsg struct {
|
||||
result string
|
||||
tokens int
|
||||
@@ -38,6 +35,7 @@ type model struct {
|
||||
input textarea.Model
|
||||
viewport viewport.Model
|
||||
spinner spinner.Model
|
||||
help help.Model
|
||||
keys KeyMap
|
||||
|
||||
targetLang string
|
||||
@@ -72,16 +70,22 @@ func NewApp(cfg *config.Config, t *translator.Translator) *tea.Program {
|
||||
sp.Spinner = spinner.MiniDot
|
||||
sp.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#8B5CF6"))
|
||||
|
||||
return tea.NewProgram(model{
|
||||
hp := help.New()
|
||||
|
||||
m := model{
|
||||
config: cfg,
|
||||
translator: t,
|
||||
messages: make([]ChatMessage, 0),
|
||||
input: ta,
|
||||
viewport: vp,
|
||||
spinner: sp,
|
||||
help: hp,
|
||||
keys: keys,
|
||||
targetLang: getDefaultLang(cfg),
|
||||
})
|
||||
}
|
||||
|
||||
p := tea.NewProgram(m)
|
||||
return p
|
||||
}
|
||||
|
||||
func getDefaultLang(cfg *config.Config) string {
|
||||
@@ -105,6 +109,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
m.help.SetWidth(msg.Width)
|
||||
m.updateLayout()
|
||||
|
||||
case translateMsg:
|
||||
@@ -142,6 +147,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case "end":
|
||||
m.viewport.GotoBottom()
|
||||
}
|
||||
if key.Matches(msg, m.keys.Help) {
|
||||
m.help.ShowAll = !m.help.ShowAll
|
||||
}
|
||||
}
|
||||
|
||||
m.input, cmd = m.input.Update(msg)
|
||||
@@ -211,7 +219,12 @@ func (m *model) updateLayout() {
|
||||
|
||||
m.input.SetWidth(contentWidth)
|
||||
m.viewport.SetWidth(contentWidth)
|
||||
m.viewport.SetHeight(m.height - 12)
|
||||
|
||||
helpLines := 2
|
||||
if m.help.ShowAll {
|
||||
helpLines = 4
|
||||
}
|
||||
m.viewport.SetHeight(m.height - 12 - helpLines)
|
||||
if m.viewport.Height() < 5 {
|
||||
m.viewport.SetHeight(10)
|
||||
}
|
||||
@@ -222,6 +235,10 @@ func (m *model) updateLayout() {
|
||||
func (m *model) updateViewportContent() {
|
||||
var b strings.Builder
|
||||
|
||||
if len(m.messages) > 0 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
for _, msg := range m.messages {
|
||||
b.WriteString(m.renderTranslationCard(msg))
|
||||
}
|
||||
@@ -257,7 +274,7 @@ func (m *model) renderTranslationCard(msg ChatMessage) string {
|
||||
Render(msg.Error)
|
||||
outputBlock = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#F87171")).
|
||||
Padding(0, 3, 1, 3).
|
||||
Padding(1, 3, 1, 3).
|
||||
Width(m.viewport.Width()).
|
||||
Render(outputContent)
|
||||
} else {
|
||||
@@ -265,7 +282,7 @@ func (m *model) renderTranslationCard(msg ChatMessage) string {
|
||||
Width(m.viewport.Width() - 2).
|
||||
Render(msg.Output)
|
||||
outputBlock = lipgloss.NewStyle().
|
||||
Padding(0, 3, 1, 3).
|
||||
Padding(1, 3, 1, 3).
|
||||
Width(m.viewport.Width()).
|
||||
Render(outputContent)
|
||||
}
|
||||
@@ -304,67 +321,26 @@ func (m model) View() tea.View {
|
||||
messages := m.viewport.View()
|
||||
inputArea := m.renderInputArea()
|
||||
infoBar := m.renderInfoBar()
|
||||
spinnerView := m.renderSpinner()
|
||||
helpView := m.help.View(m.keys)
|
||||
|
||||
content := header + "\n" + messages + inputArea + infoBar + spinnerView
|
||||
content := header + "\n" + messages + inputArea + infoBar + helpView
|
||||
v := tea.NewView(content)
|
||||
v.AltScreen = true
|
||||
return v
|
||||
}
|
||||
|
||||
func gradientText(text string, startColor, endColor string) string {
|
||||
startR, startG, startB := parseHexColor(startColor)
|
||||
endR, endG, endB := parseHexColor(endColor)
|
||||
|
||||
lines := strings.Split(text, "\n")
|
||||
if len(lines) == 0 {
|
||||
return text
|
||||
}
|
||||
|
||||
var result []string
|
||||
for _, line := range lines {
|
||||
if len(line) == 0 {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
|
||||
var coloredLine string
|
||||
for i, char := range line {
|
||||
ratio := float64(i) / float64(len(line)-1)
|
||||
r := int(float64(startR) + float64(endR-startR)*ratio)
|
||||
g := int(float64(startG) + float64(endG-startG)*ratio)
|
||||
b := int(float64(startB) + float64(endB-startB)*ratio)
|
||||
coloredLine += fmt.Sprintf("\033[38;2;%d;%d;%dm%s\033[0m", r, g, b, string(char))
|
||||
}
|
||||
result = append(result, coloredLine)
|
||||
}
|
||||
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
func parseHexColor(hex string) (int, int, int) {
|
||||
hex = strings.TrimPrefix(hex, "#")
|
||||
r, _ := strconv.ParseInt(hex[0:2], 16, 64)
|
||||
g, _ := strconv.ParseInt(hex[2:4], 16, 64)
|
||||
b, _ := strconv.ParseInt(hex[4:6], 16, 64)
|
||||
return int(r), int(g), int(b)
|
||||
}
|
||||
|
||||
func (m model) renderHeader() string {
|
||||
title := gradientText(logoPattern, "#8B5CF6", "#EC4899")
|
||||
title := logo.GradientText(logo.GetLogoPattern(), "#B413DC", "#00C8C8")
|
||||
titleWithVersion := title + logo.GetVersionSuffix()
|
||||
|
||||
width := m.width - 4
|
||||
if width < 20 {
|
||||
width = 60
|
||||
}
|
||||
|
||||
right := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#6B7280")).
|
||||
Render("[Ctrl+C 退出]")
|
||||
|
||||
return lipgloss.NewStyle().
|
||||
Width(width).
|
||||
Render(title + strings.Repeat(" ", width-29-len(right)-1) + right)
|
||||
Render(titleWithVersion)
|
||||
}
|
||||
|
||||
func (m model) renderInputArea() string {
|
||||
|
||||
43
memory.md
43
memory.md
@@ -894,4 +894,45 @@ v2:
|
||||
```go
|
||||
p := tea.NewProgram(model{})
|
||||
p.Run()
|
||||
```
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Logo模块化设计
|
||||
|
||||
**决策**: 创建 `internal/logo/logo.go` 统一管理logo,TUI和CLI共享
|
||||
|
||||
**原因**:
|
||||
1. 避免代码重复:TUI和CLI都需要显示logo
|
||||
2. 统一渐变色方案:紫→青 (`#B413DC` → `#00C8C8`)
|
||||
3. 统一版本号格式:` ( v1.x.x )` 或 ` ( )`
|
||||
|
||||
**实现**:
|
||||
```go
|
||||
// 导出函数供外部调用
|
||||
func GradientText(text string, startColor, endColor string) string
|
||||
func GetLogoPattern() string // ASCII art图案
|
||||
func GetVersionSuffix() string // " (v1.x.x )" 或 " ( )"
|
||||
func PrintLogoWithVersion() // 打印完整logo
|
||||
```
|
||||
|
||||
**版本注入**:
|
||||
```go
|
||||
// 编译时通过 ldflags 注入
|
||||
// -X packagepath.variable=value
|
||||
go build -ldflags "-X github.com/titor/fanyi/internal/logo.version=${VERSION}" -o yoyo ./cmd/yoyo
|
||||
```
|
||||
|
||||
### 终端颜色输出问题
|
||||
|
||||
**问题**: 在非TTY环境(如管道)下,ANSI转义序列可能显示为明文
|
||||
|
||||
**观察**:
|
||||
- 使用 `script` 命令可以正确显示颜色
|
||||
- `od -c` 检查输出包含正确的 `\033` 转义字符
|
||||
- zsh 可能对某些颜色转义处理不同
|
||||
|
||||
**解决方案**:
|
||||
- 使用 `GradientText` 函数逐字符应用渐变
|
||||
- 每个字符后使用 `\033[0m` 重置
|
||||
- 渐变使用 24-bit 颜色 `\033[38;2;R;G;Bm`
|
||||
130
taolun.md
130
taolun.md
@@ -794,4 +794,132 @@ ta.SetHeight(5) // 固定高度,不动态调整
|
||||
- 遵循语义化版本:主版本.次版本.修订版本
|
||||
- beta版使用 `-beta` 后缀
|
||||
|
||||
**关联版本**: [changelog.md#1.0.0-beta](changelog.md#100-beta-2026-04-07)
|
||||
**关联版本**: [changelog.md#1.0.0-beta](changelog.md#100-beta-2026-04-07)
|
||||
|
||||
---
|
||||
|
||||
### [2026-04-07] 版本 1.1.0 - 配置路径修复和huh迁移
|
||||
|
||||
**原因**:
|
||||
1. 管道模式下(如 `cd /docs && cat readme.md | yoyo`)找不到配置文件
|
||||
2. onboard配置保存到错误的相对路径
|
||||
3. 希望用 `charmbracelet/huh` 替代 `survey` 获得更好的UX
|
||||
|
||||
**分析**:
|
||||
- 所有配置路径硬编码为 `configs/config.yaml`(相对CWD)
|
||||
- 从不同目录运行程序时路径解析失败
|
||||
- survey库API较老,huh提供更现代的表单体验
|
||||
|
||||
**解决方案**:
|
||||
1. 新增 `internal/config/path.go` 路径解析工具
|
||||
2. 配置查找优先级:`--config` > `~/.config/yoyo/config.yaml` > `./configs/config.yaml`
|
||||
3. onboard保存到 `~/.config/yoyo/config.yaml`
|
||||
4. .env从 `~/.config/yoyo/.env` 加载
|
||||
5. onboard使用huh重写:Form+Group模式,链式API,泛型支持
|
||||
|
||||
**技术细节**:
|
||||
```go
|
||||
// 路径解析
|
||||
config.ResolveConfigPath(userPath) // 智能查找配置
|
||||
config.GetUserConfigPath() // ~/.config/yoyo/config.yaml
|
||||
config.GetUserEnvPath() // ~/.config/yoyo/.env
|
||||
```
|
||||
|
||||
**huh迁移要点**:
|
||||
- `survey.Select` → `huh.NewSelect[string]().Options(huh.NewOption(...)...)`
|
||||
- `survey.Input` → `huh.NewInput().Value(&var).Validate(fn)`
|
||||
- `survey.Confirm` → `huh.NewConfirm().Affirmative("是").Negative("否")`
|
||||
- 分步表单 → `huh.NewForm(huh.NewGroup(...), huh.NewGroup(...))`
|
||||
|
||||
**关联版本**: [changelog.md#1.1.0](changelog.md#110-2026-04-07)
|
||||
|
||||
---
|
||||
|
||||
### [2026-04-08 00:40] 版本 1.1.1 - Logo模块化与渐变色统一
|
||||
**原因**:
|
||||
1. `--help` `-h` `-?` `--version` 在默认交互式模式下无响应
|
||||
2. 帮助信息头部需要彩色logo展示
|
||||
|
||||
**问题分析**:
|
||||
1. 交互模式判断优先于help/version检查,导致flags被忽略
|
||||
2. 帮助信息使用硬编码版本号 `YOYO翻译工具 v1.1.0`,无logo
|
||||
|
||||
**解决方案**:
|
||||
1. 修复flag检查顺序:在进入交互模式前先检查 `-h` `--help` `-?` `--version`
|
||||
2. 新增 `internal/logo/logo.go` 模块统一管理logo
|
||||
3. 编译时通过 `ldflags` 注入版本号
|
||||
4. 渐变色使用与TUI一致的紫→青色方案
|
||||
|
||||
**技术实现**:
|
||||
```go
|
||||
// logo模块核心函数
|
||||
func GradientText(text string, startColor, endColor string) string { ... }
|
||||
func GetLogoPattern() string // 返回4行ascii art
|
||||
func GetVersionSuffix() string // 返回 " (v1.1.1-dirty )" 或 " ( )"
|
||||
func PrintLogoWithVersion() // 打印完整logo(--help/--version使用)
|
||||
```
|
||||
|
||||
**build.sh版本注入**:
|
||||
```bash
|
||||
VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "")
|
||||
go build -ldflags "-X github.com/titor/fanyi/internal/logo.version=${VERSION}" -o yoyo ./cmd/yoyo
|
||||
```
|
||||
|
||||
**TUI整合**:
|
||||
- 移除 `internal/tui/model.go` 中的本地 `logoPattern` 和 `gradientText`
|
||||
- 直接调用 `logo.GradientText(logo.GetLogoPattern(), "#B413DC", "#00C8C8")`
|
||||
- 移除 `[Ctrl+C 退出]` 显示
|
||||
|
||||
**最终输出格式**(所有位置统一):
|
||||
```
|
||||
_ _ _____ _____
|
||||
( \/ ( _ ( _ )
|
||||
\ / )(_)( )(_)(
|
||||
(__)(_____(_____( v1.1.1-dirty )
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [2026-04-08] TUI界面帮助功能与样式改进
|
||||
|
||||
**原因**:
|
||||
1. 用户需要在TUI底部添加帮助信息栏
|
||||
2. 优化翻译卡片的显示样式
|
||||
|
||||
**分析**:
|
||||
- 使用 bubbletea 的 help 组件实现帮助栏
|
||||
- 翻译卡片需要与输入区域保持间距
|
||||
|
||||
**解决方案**:
|
||||
|
||||
1. **帮助功能实现**:
|
||||
- 导入 `charm.land/bubbles/v2/help`
|
||||
- 在 `model` 结构体添加 `help help.Model` 字段
|
||||
- `KeyMap` 实现 `ShortHelp()` 和 `FullHelp()` 接口
|
||||
- 按键绑定: Help -> Ctrl+H (原 ? 和 ctrl+? 不兼容)
|
||||
- View 中添加 help.View(m.keys) 到底部
|
||||
|
||||
2. **翻译卡片样式**:
|
||||
- 翻译结果 outputBlock 的 Padding 从 `(0,3,1,3)` 改为 `(1,3,1,3)` 增加上方空隙
|
||||
- viewport 内容区域第一个卡片前添加 `"\n"` 作为上边距
|
||||
|
||||
3. **版本号注入**:
|
||||
- 扩展 build.sh 支持跨平台编译
|
||||
- 添加 -h 帮助选项
|
||||
- 添加 -o 自定义输出文件名
|
||||
- release.yaml 使用 build.sh 构建
|
||||
|
||||
**按键绑定历史**:
|
||||
- 初始: `?` → shift+? 切换,且会输入到文本框
|
||||
- 尝试: `ctrl+?` → 不兼容,无响应
|
||||
- 最终: `ctrl+h` → 正常工作
|
||||
|
||||
**相关文件**:
|
||||
- `internal/tui/keys.go` - KeyMap 定义和 ShortHelp/FullHelp
|
||||
- `internal/tui/model.go` - help 组件集成、样式调整
|
||||
- `build.sh` - 跨平台编译支持
|
||||
- `.gitea/workflows/release.yaml` - CI 构建脚本
|
||||
|
||||
**关联版本**: [changelog.md#0.7.1](changelog.md#071-2026-04-08---tui界面改进)
|
||||
|
||||
**关联版本**: [changelog.md#1.1.1](changelog.md#111-2026-04-08)
|
||||
Reference in New Issue
Block a user