Compare commits
20 Commits
v0.8.2
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
| d26558ad0a | |||
| c2c3b11d35 | |||
| 34a7e7d208 | |||
| d327bedf04 | |||
| 8c5eff79cc | |||
| 0ba6df60b0 | |||
| 1733d7b1cc | |||
| a4f3f1fef3 | |||
| 9b0a5dcb4c | |||
| 15759ec42d | |||
| 6fbf9a68cf | |||
| cdf661734f | |||
| 4b12c90e50 | |||
| a3bc91bdaf | |||
| 1d12134314 | |||
| cd90a9c1b3 | |||
| 60c9f99525 | |||
| aded7dba33 | |||
| 917002834c | |||
| b04092fd68 |
57
.gitea/workflows/release.yaml
Normal file
57
.gitea/workflows/release.yaml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: golang:1.26-alpine
|
||||||
|
env:
|
||||||
|
GOPROXY: "https://mirrors.aliyun.com/goproxy/,direct"
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
run: |
|
||||||
|
apk add git
|
||||||
|
git clone -b dev 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: |
|
||||||
|
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
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Checksums
|
||||||
|
run: sha256sum yoo-* > checksums.txt
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
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" \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\":\"${TAG_NAME}\",\"name\":\"${TAG_NAME}\",\"body\":\"Automated release\"}"
|
||||||
|
|
||||||
|
# 上传产物
|
||||||
|
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}" \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-F "attachment=@$f"
|
||||||
|
done
|
||||||
29
changelog.md
29
changelog.md
@@ -441,4 +441,31 @@ yoyo onboard --force
|
|||||||
- View() 方法返回 `tea.View` 类型
|
- View() 方法返回 `tea.View` 类型
|
||||||
- KeyMsg 改为 KeyPressMsg,使用 `msg.String()` 判断键位
|
- KeyMsg 改为 KeyPressMsg,使用 `msg.String()` 判断键位
|
||||||
|
|
||||||
**讨论记录**: [taolun.md#版本-0.8.1-翻译结果卡片组件设计](taolun.md#版本-081---翻译结果卡片组件设计)
|
**讨论记录**: [taolun.md#版本-0.8.1-翻译结果卡片组件设计](taolun.md#版本-081---翻译结果卡片组件设计)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.0.0-beta (2026-04-07)
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- ASCII艺术Logo标题,带紫色→粉色渐变效果
|
||||||
|
- 输入框改造:
|
||||||
|
- 使用 `:::` 紫色分隔符替代上下边框
|
||||||
|
- Ctrl+J 换行功能
|
||||||
|
- 信息栏改造:
|
||||||
|
- 合并显示:语言、模型名、缓存记录数
|
||||||
|
- 添加翻译状态 Spinner 动画 (MiniDot)
|
||||||
|
- 翻译结果卡片优化:
|
||||||
|
- 底部 `▣` 图标左侧边距从3减少到2
|
||||||
|
- `▣` 与文字间距从3减少到2
|
||||||
|
|
||||||
|
### 技术细节
|
||||||
|
- 使用 lipgloss 实现 True Color 渐变效果
|
||||||
|
- 使用 charmbracelet/bubbles spinner 组件实现加载动画
|
||||||
|
- 版本号显示在Logo右侧 [v1.0.0-beta]
|
||||||
|
- 动态调整 viewport 高度适应终端
|
||||||
|
|
||||||
|
### 版本号规则
|
||||||
|
- 版本号需与 git 标签、changelog.md 中的版本号保持三方同步
|
||||||
|
|
||||||
|
**讨论记录**: [taolun.md#版本-100-beta-Logo和信息栏改造](taolun.md#版本-100-beta-logo和信息栏改造)
|
||||||
553
cmd/yoyo/main.go
Normal file
553
cmd/yoyo/main.go
Normal file
@@ -0,0 +1,553 @@
|
|||||||
|
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/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, "显示帮助信息")
|
||||||
|
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的短格式)")
|
||||||
|
)
|
||||||
|
|
||||||
|
const versionString = "YOYO翻译工具 v0.5.1"
|
||||||
|
|
||||||
|
// 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 *interactive || *interactiveShort || shouldStartInteractive() {
|
||||||
|
startInteractiveMode()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理管道输入情况
|
||||||
|
if isPipeInput() {
|
||||||
|
// 管道模式下,即使没有参数也继续执行
|
||||||
|
if *help || *h {
|
||||||
|
printHelp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 非管道模式下,没有参数则显示帮助
|
||||||
|
if *help || *h || 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() // 忽略错误,如果文件不存在
|
||||||
|
|
||||||
|
// 加载配置
|
||||||
|
configPath := *configFile
|
||||||
|
if configPath == "" {
|
||||||
|
configPath = "configs/config.yaml" // 默认配置文件路径
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
// 加载配置
|
||||||
|
configPath := "configs/config.yaml"
|
||||||
|
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() {
|
||||||
|
fmt.Println(versionString)
|
||||||
|
}
|
||||||
|
|
||||||
|
// printHelp 显示帮助信息
|
||||||
|
func printHelp() {
|
||||||
|
fmt.Printf(`%s
|
||||||
|
|
||||||
|
使用方法:
|
||||||
|
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 # 启动交互式翻译界面(短格式)
|
||||||
|
|
||||||
|
配置:
|
||||||
|
- 配置文件: configs/config.yaml
|
||||||
|
- 环境变量: .env 文件
|
||||||
|
- 默认厂商: siliconflow
|
||||||
|
- 默认目标语言: zh-CN (简体中文)
|
||||||
|
|
||||||
|
更多信息请访问: https://github.com/titor/fanyi
|
||||||
|
`, versionString)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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() // 忽略错误,如果文件不存在
|
||||||
|
|
||||||
|
// 加载配置
|
||||||
|
configPath := *configFile
|
||||||
|
if configPath == "" {
|
||||||
|
configPath = "configs/config.yaml" // 默认配置文件路径
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Version = "1.0.0"
|
Version = "1.0.0-beta"
|
||||||
)
|
)
|
||||||
|
|
||||||
func DetectLanguage(text string) string {
|
func DetectLanguage(text string) string {
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ type Translator struct {
|
|||||||
cache cache.Cache
|
cache cache.Cache
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Translator) GetCache() cache.Cache {
|
||||||
|
return t.cache
|
||||||
|
}
|
||||||
|
|
||||||
// NewTranslator 创建翻译器实例
|
// NewTranslator 创建翻译器实例
|
||||||
func NewTranslator(config *config.Config, provider provider.Provider) *Translator {
|
func NewTranslator(config *config.Config, provider provider.Provider) *Translator {
|
||||||
translator := &Translator{
|
translator := &Translator{
|
||||||
|
|||||||
@@ -3,19 +3,27 @@ package tui
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"charm.land/bubbles/v2/spinner"
|
||||||
"charm.land/bubbles/v2/textarea"
|
"charm.land/bubbles/v2/textarea"
|
||||||
"charm.land/bubbles/v2/viewport"
|
"charm.land/bubbles/v2/viewport"
|
||||||
"charm.land/bubbletea/v2"
|
"charm.land/bubbletea/v2"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/titor/fanyi/internal/config"
|
"github.com/titor/fanyi/internal/config"
|
||||||
|
"github.com/titor/fanyi/internal/content"
|
||||||
"github.com/titor/fanyi/internal/translator"
|
"github.com/titor/fanyi/internal/translator"
|
||||||
)
|
)
|
||||||
|
|
||||||
var supportedLangs = []string{"zh-CN", "en-US", "ja", "ko", "zh-TW", "es", "fr", "de"}
|
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 {
|
type translateMsg struct {
|
||||||
result string
|
result string
|
||||||
tokens int
|
tokens int
|
||||||
@@ -29,6 +37,7 @@ type model struct {
|
|||||||
messages []ChatMessage
|
messages []ChatMessage
|
||||||
input textarea.Model
|
input textarea.Model
|
||||||
viewport viewport.Model
|
viewport viewport.Model
|
||||||
|
spinner spinner.Model
|
||||||
keys KeyMap
|
keys KeyMap
|
||||||
|
|
||||||
targetLang string
|
targetLang string
|
||||||
@@ -45,25 +54,31 @@ func NewApp(cfg *config.Config, t *translator.Translator) *tea.Program {
|
|||||||
keys := NewKeyMap()
|
keys := NewKeyMap()
|
||||||
|
|
||||||
ta := textarea.New()
|
ta := textarea.New()
|
||||||
ta.Placeholder = "输入要翻译的文本... (Ctrl+J 换行)"
|
ta.Placeholder = "在这里输入你要翻译的内容。(Enter 翻译,Ctrl+J 启用换行)"
|
||||||
ta.Focus()
|
ta.Focus()
|
||||||
ta.Prompt = ""
|
ta.Prompt = ""
|
||||||
ta.ShowLineNumbers = false
|
ta.ShowLineNumbers = false
|
||||||
ta.SetWidth(60)
|
ta.SetWidth(60)
|
||||||
ta.SetHeight(5)
|
ta.SetHeight(5)
|
||||||
ta.SetStyles(textarea.DefaultStyles(false))
|
ta.SetStyles(textarea.DefaultStyles(true))
|
||||||
|
|
||||||
|
ta.KeyMap.InsertNewline.SetKeys("ctrl+j")
|
||||||
ta.KeyMap.InsertNewline.SetEnabled(true)
|
ta.KeyMap.InsertNewline.SetEnabled(true)
|
||||||
|
|
||||||
vp := viewport.New(viewport.WithWidth(50), viewport.WithHeight(20))
|
vp := viewport.New(viewport.WithWidth(50), viewport.WithHeight(20))
|
||||||
vp.SetContent("")
|
vp.SetContent("")
|
||||||
|
|
||||||
|
sp := spinner.New()
|
||||||
|
sp.Spinner = spinner.MiniDot
|
||||||
|
sp.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#8B5CF6"))
|
||||||
|
|
||||||
return tea.NewProgram(model{
|
return tea.NewProgram(model{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
translator: t,
|
translator: t,
|
||||||
messages: make([]ChatMessage, 0),
|
messages: make([]ChatMessage, 0),
|
||||||
input: ta,
|
input: ta,
|
||||||
viewport: vp,
|
viewport: vp,
|
||||||
|
spinner: sp,
|
||||||
keys: keys,
|
keys: keys,
|
||||||
targetLang: getDefaultLang(cfg),
|
targetLang: getDefaultLang(cfg),
|
||||||
})
|
})
|
||||||
@@ -77,7 +92,7 @@ func getDefaultLang(cfg *config.Config) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m model) Init() tea.Cmd {
|
func (m model) Init() tea.Cmd {
|
||||||
return nil
|
return m.spinner.Tick
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
@@ -135,6 +150,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.viewport, cmd = m.viewport.Update(msg)
|
m.viewport, cmd = m.viewport.Update(msg)
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
|
m.spinner, cmd = m.spinner.Update(msg)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
return m, tea.Batch(cmds...)
|
return m, tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,34 +231,24 @@ func (m *model) updateViewportContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *model) renderTranslationCard(msg ChatMessage) string {
|
func (m *model) renderTranslationCard(msg ChatMessage) string {
|
||||||
metaContent := lipgloss.JoinHorizontal(
|
contentWidth := m.viewport.Width() - 2
|
||||||
lipgloss.Left,
|
|
||||||
CardMetaStyle.Render(fmt.Sprintf("Tokens: %d", msg.Tokens)),
|
|
||||||
CardMetaSeparatorStyle,
|
|
||||||
CardMetaStyle.Render(fmt.Sprintf("耗时: %s", msg.Timestamp.Format("15:04:05"))),
|
|
||||||
CardMetaSeparatorStyle,
|
|
||||||
CardMetaStyle.Render(fmt.Sprintf("模型: %s", msg.Model)),
|
|
||||||
)
|
|
||||||
|
|
||||||
metaBlock := lipgloss.NewStyle().
|
timeStr := msg.Timestamp.Format("15:04")
|
||||||
BorderStyle(lipgloss.Border{
|
timeLabel := CardTimeStyle.Render("# " + timeStr)
|
||||||
Top: "─",
|
|
||||||
Bottom: "─",
|
|
||||||
Left: "│",
|
|
||||||
Right: "│",
|
|
||||||
}).
|
|
||||||
BorderForeground(lipgloss.Color("#374151")).
|
|
||||||
Width(m.viewport.Width() - 2).
|
|
||||||
Render(metaContent)
|
|
||||||
|
|
||||||
inputContent := lipgloss.NewStyle().
|
inputText := lipgloss.NewStyle().
|
||||||
Width(m.viewport.Width() - 2).
|
Width(contentWidth).
|
||||||
Render(msg.Input)
|
Render(msg.Input)
|
||||||
|
|
||||||
inputBlock := lipgloss.NewStyle().
|
inputBlock := lipgloss.NewStyle().
|
||||||
Background(lipgloss.Color("#1A1A1A")).
|
Background(lipgloss.Color("#1A1A1A")).
|
||||||
|
Padding(1, 3).
|
||||||
Width(m.viewport.Width()).
|
Width(m.viewport.Width()).
|
||||||
Render(inputContent)
|
Render(lipgloss.JoinVertical(
|
||||||
|
lipgloss.Top,
|
||||||
|
timeLabel,
|
||||||
|
inputText,
|
||||||
|
))
|
||||||
|
|
||||||
var outputBlock string
|
var outputBlock string
|
||||||
if msg.Error != "" {
|
if msg.Error != "" {
|
||||||
@@ -249,6 +257,7 @@ func (m *model) renderTranslationCard(msg ChatMessage) string {
|
|||||||
Render(msg.Error)
|
Render(msg.Error)
|
||||||
outputBlock = lipgloss.NewStyle().
|
outputBlock = lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#F87171")).
|
Foreground(lipgloss.Color("#F87171")).
|
||||||
|
Padding(0, 3, 1, 3).
|
||||||
Width(m.viewport.Width()).
|
Width(m.viewport.Width()).
|
||||||
Render(outputContent)
|
Render(outputContent)
|
||||||
} else {
|
} else {
|
||||||
@@ -256,16 +265,25 @@ func (m *model) renderTranslationCard(msg ChatMessage) string {
|
|||||||
Width(m.viewport.Width() - 2).
|
Width(m.viewport.Width() - 2).
|
||||||
Render(msg.Output)
|
Render(msg.Output)
|
||||||
outputBlock = lipgloss.NewStyle().
|
outputBlock = lipgloss.NewStyle().
|
||||||
|
Padding(0, 3, 1, 3).
|
||||||
Width(m.viewport.Width()).
|
Width(m.viewport.Width()).
|
||||||
Render(outputContent)
|
Render(outputContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
footerContent := lipgloss.NewStyle().
|
||||||
|
MarginLeft(3).
|
||||||
|
Render(lipgloss.JoinHorizontal(
|
||||||
|
lipgloss.Left,
|
||||||
|
CardFooterIconStyle.Render("▣"),
|
||||||
|
CardFooterTextStyle.Render(msg.Model+" · "+strconv.Itoa(msg.Tokens)),
|
||||||
|
))
|
||||||
|
|
||||||
return CardStyle.Render(
|
return CardStyle.Render(
|
||||||
lipgloss.JoinVertical(
|
lipgloss.JoinVertical(
|
||||||
lipgloss.Top,
|
lipgloss.Top,
|
||||||
metaBlock,
|
|
||||||
inputBlock,
|
inputBlock,
|
||||||
outputBlock,
|
outputBlock,
|
||||||
|
footerContent,
|
||||||
),
|
),
|
||||||
) + "\n"
|
) + "\n"
|
||||||
}
|
}
|
||||||
@@ -285,19 +303,55 @@ func (m model) View() tea.View {
|
|||||||
header := m.renderHeader()
|
header := m.renderHeader()
|
||||||
messages := m.viewport.View()
|
messages := m.viewport.View()
|
||||||
inputArea := m.renderInputArea()
|
inputArea := m.renderInputArea()
|
||||||
statusBar := m.renderStatusBar()
|
infoBar := m.renderInfoBar()
|
||||||
|
spinnerView := m.renderSpinner()
|
||||||
|
|
||||||
content := header + "\n" + messages + inputArea + statusBar
|
content := header + "\n" + messages + inputArea + infoBar + spinnerView
|
||||||
v := tea.NewView(content)
|
v := tea.NewView(content)
|
||||||
v.AltScreen = true
|
v.AltScreen = true
|
||||||
return v
|
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 {
|
func (m model) renderHeader() string {
|
||||||
title := lipgloss.NewStyle().
|
title := gradientText(logoPattern, "#8B5CF6", "#EC4899")
|
||||||
Foreground(lipgloss.Color("#8B5CF6")).
|
|
||||||
Bold(true).
|
|
||||||
Render("✦ YOYO 翻译")
|
|
||||||
|
|
||||||
width := m.width - 4
|
width := m.width - 4
|
||||||
if width < 20 {
|
if width < 20 {
|
||||||
@@ -310,57 +364,50 @@ func (m model) renderHeader() string {
|
|||||||
|
|
||||||
return lipgloss.NewStyle().
|
return lipgloss.NewStyle().
|
||||||
Width(width).
|
Width(width).
|
||||||
Render(title + strings.Repeat(" ", width-len(title)-len(right)-1) + right)
|
Render(title + strings.Repeat(" ", width-29-len(right)-1) + right)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) renderInputArea() string {
|
func (m model) renderInputArea() string {
|
||||||
inputView := m.input.View()
|
inputView := m.input.View()
|
||||||
|
separator := lipgloss.NewStyle().
|
||||||
container := lipgloss.NewStyle().
|
Foreground(lipgloss.Color("#8B5CF6")).
|
||||||
Width(m.input.Width() + 1).
|
Render(":::")
|
||||||
BorderStyle(lipgloss.Border{
|
return "\n" + separator + "\n" + inputView + "\n"
|
||||||
Top: "─",
|
|
||||||
Bottom: "─",
|
|
||||||
Left: "│",
|
|
||||||
Right: "│",
|
|
||||||
}).
|
|
||||||
BorderForeground(lipgloss.Color("#60A5FA"))
|
|
||||||
|
|
||||||
return "\n" + container.Render(inputView) + "\n"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) renderStatusBar() string {
|
func (m model) renderInfoBar() string {
|
||||||
langInfo := "目标: " + m.targetLang
|
var recordCount int
|
||||||
modelInfo := "模型: " + getModelName(m.config)
|
if m.translator != nil && m.translator.GetCache() != nil {
|
||||||
tokensInfo := "Tokens: -"
|
stats, _ := m.translator.GetCache().Stats(context.Background())
|
||||||
if len(m.messages) > 0 {
|
if stats != nil {
|
||||||
lastMsg := m.messages[len(m.messages)-1]
|
recordCount = stats.TotalRecords
|
||||||
if lastMsg.Tokens > 0 {
|
|
||||||
tokensInfo = fmt.Sprintf("Tokens: %d", lastMsg.Tokens)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statusDot := StatusDotStyle.Render("●")
|
var countInfo string
|
||||||
|
if recordCount == 0 {
|
||||||
|
countInfo = "暂无记录"
|
||||||
|
} else {
|
||||||
|
countInfo = fmt.Sprintf("%d条", recordCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
sep := lipgloss.NewStyle().Foreground(lipgloss.Color("#8B5CF6")).Render(" ")
|
||||||
|
separator := lipgloss.NewStyle().Foreground(lipgloss.Color("#8B5CF6")).Render(":::")
|
||||||
|
lang := lipgloss.NewStyle().Foreground(lipgloss.Color("#F87171")).Render(m.targetLang)
|
||||||
|
model := lipgloss.NewStyle().Foreground(lipgloss.Color("#FAFAFA")).Render(getModelName(m.config))
|
||||||
|
count := lipgloss.NewStyle().Foreground(lipgloss.Color("#6B7280")).Render(countInfo)
|
||||||
|
|
||||||
|
result := separator + sep + lang + sep + model + sep + count
|
||||||
|
|
||||||
if m.loading {
|
if m.loading {
|
||||||
statusDot = LoadingStyle.Render("○")
|
result += sep + m.spinner.View() + " 翻译中..."
|
||||||
}
|
}
|
||||||
|
|
||||||
sep := StatusItemStyle.Render(" │ ")
|
return result + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
width := m.width - 4
|
func (m model) renderSpinner() string {
|
||||||
if width < 30 {
|
return ""
|
||||||
width = 60
|
|
||||||
}
|
|
||||||
|
|
||||||
status := StatusItemStyle.Render(langInfo) +
|
|
||||||
sep + StatusItemStyle.Render(modelInfo) +
|
|
||||||
sep + StatusItemStyle.Render(tokensInfo) +
|
|
||||||
sep + statusDot + " " + StatusValueStyle.Render(m.getStatusText())
|
|
||||||
|
|
||||||
return lipgloss.NewStyle().
|
|
||||||
Width(width).
|
|
||||||
Background(lipgloss.Color("#1F2937")).
|
|
||||||
Render(" " + status)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) getStatusText() string {
|
func (m model) getStatusText() string {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ var (
|
|||||||
Foreground(lipgloss.Color("#34D399"))
|
Foreground(lipgloss.Color("#34D399"))
|
||||||
|
|
||||||
CardStyle = lipgloss.NewStyle().
|
CardStyle = lipgloss.NewStyle().
|
||||||
MarginBottom(5)
|
MarginBottom(1)
|
||||||
|
|
||||||
CardMetaStyle = lipgloss.NewStyle().
|
CardMetaStyle = lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#6B7280")).
|
Foreground(lipgloss.Color("#6B7280")).
|
||||||
@@ -68,4 +68,17 @@ var (
|
|||||||
|
|
||||||
CardOutputStyle = lipgloss.NewStyle().
|
CardOutputStyle = lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#FFFFFF"))
|
Foreground(lipgloss.Color("#FFFFFF"))
|
||||||
|
|
||||||
|
CardTimeStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#6B7280")).
|
||||||
|
MarginBottom(1)
|
||||||
|
|
||||||
|
CardFooterIconStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#8B5CF6")).
|
||||||
|
AlignVertical(lipgloss.Center)
|
||||||
|
|
||||||
|
CardFooterTextStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#6B7280")).
|
||||||
|
MarginLeft(2).
|
||||||
|
AlignVertical(lipgloss.Center)
|
||||||
)
|
)
|
||||||
|
|||||||
38
taolun.md
38
taolun.md
@@ -758,4 +758,40 @@ ta.SetHeight(5) // 固定高度,不动态调整
|
|||||||
**下一步**: 实现组件代码
|
**下一步**: 实现组件代码
|
||||||
|
|
||||||
**关联文档**:
|
**关联文档**:
|
||||||
- [changelog.md#0.8.1](changelog.md#081)
|
- [changelog.md#0.8.1](changelog.md#081)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [2026-04-07] 版本 1.0.0-beta - Logo和信息栏改造
|
||||||
|
|
||||||
|
**原因**: 用户希望改进TUI界面的视觉效果,使标题更独特,输入框和信息栏更美观
|
||||||
|
|
||||||
|
**分析**:
|
||||||
|
- 原标题 "✦ YOYO 翻译" 过于简单
|
||||||
|
- 输入框需要更好的视觉分隔
|
||||||
|
- 需要添加翻译状态动画
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
1. **标题Logo**:
|
||||||
|
- 使用ASCII艺术 "l_ _ _____ _____"
|
||||||
|
- 实现紫色→粉色渐变效果 (ANSI True Color)
|
||||||
|
- 右侧显示版本号 [v1.0.0-beta]
|
||||||
|
|
||||||
|
2. **输入框改造**:
|
||||||
|
- 去掉边框
|
||||||
|
- 上下使用紫色 `:::` 分隔符
|
||||||
|
- Ctrl+J 启用换行
|
||||||
|
|
||||||
|
3. **信息栏改造**:
|
||||||
|
- 合并显示:语言(红色) + 模型名(白色) + 缓存记录(碳黑)
|
||||||
|
- 翻译时显示 Spinner 动画 (MiniDot)
|
||||||
|
|
||||||
|
4. **翻译卡片优化**:
|
||||||
|
- `▣` 图标边距调整
|
||||||
|
|
||||||
|
**版本号规则**:
|
||||||
|
- 版本号需与 git 标签、changelog.md 中的版本号保持三方同步
|
||||||
|
- 遵循语义化版本:主版本.次版本.修订版本
|
||||||
|
- beta版使用 `-beta` 后缀
|
||||||
|
|
||||||
|
**关联版本**: [changelog.md#1.0.0-beta](changelog.md#100-beta-2026-04-07)
|
||||||
Reference in New Issue
Block a user