Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 13298931fd | |||
| 0a40258d9a | |||
| a9b7a69224 |
@@ -25,12 +25,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
|
cd /workspace/titor/yoyo
|
||||||
|
chmod +x ./build.sh
|
||||||
for p in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64; do
|
for p in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64; do
|
||||||
os=${p%/*}
|
os=${p%/*}
|
||||||
arch=${p#*/}
|
arch=${p#*/}
|
||||||
ext=""
|
ext=""
|
||||||
[ "$os" = "windows" ] && ext=".exe"
|
[ "$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
|
done
|
||||||
|
|
||||||
- name: Checksums
|
- name: Checksums
|
||||||
|
|||||||
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"
|
||||||
66
changelog.md
66
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界面改进
|
### 0.7.0 (2026-04-06) - TUI界面改进
|
||||||
**类型**: 功能版本
|
**类型**: 功能版本
|
||||||
**状态**: 开发中
|
**状态**: 开发中
|
||||||
@@ -498,4 +523,43 @@ yoyo onboard --force
|
|||||||
- 支持 `~` 路径展开
|
- 支持 `~` 路径展开
|
||||||
- huh使用v2版本,支持泛型和链式API
|
- huh使用v2版本,支持泛型和链式API
|
||||||
|
|
||||||
**讨论记录**: [taolun.md#2026-04-07-配置路径修复和huh迁移](taolun.md)
|
**讨论记录**: [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)
|
||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/titor/fanyi/internal/cache"
|
"github.com/titor/fanyi/internal/cache"
|
||||||
"github.com/titor/fanyi/internal/config"
|
"github.com/titor/fanyi/internal/config"
|
||||||
"github.com/titor/fanyi/internal/lang"
|
"github.com/titor/fanyi/internal/lang"
|
||||||
|
"github.com/titor/fanyi/internal/logo"
|
||||||
"github.com/titor/fanyi/internal/onboard"
|
"github.com/titor/fanyi/internal/onboard"
|
||||||
"github.com/titor/fanyi/internal/provider"
|
"github.com/titor/fanyi/internal/provider"
|
||||||
"github.com/titor/fanyi/internal/translator"
|
"github.com/titor/fanyi/internal/translator"
|
||||||
@@ -25,6 +26,7 @@ var (
|
|||||||
version = flag.Bool("version", false, "显示版本信息")
|
version = flag.Bool("version", false, "显示版本信息")
|
||||||
help = flag.Bool("help", false, "显示帮助信息")
|
help = flag.Bool("help", false, "显示帮助信息")
|
||||||
h = flag.Bool("h", false, "显示帮助信息")
|
h = flag.Bool("h", false, "显示帮助信息")
|
||||||
|
question = flag.Bool("?", false, "显示帮助信息")
|
||||||
langFlag = flag.String("lang", "", "目标语言代码(如 zh-CN, en-US, cn, en 等)")
|
langFlag = flag.String("lang", "", "目标语言代码(如 zh-CN, en-US, cn, en 等)")
|
||||||
langLong = flag.String("language", "", "目标语言代码(--lang的长格式)")
|
langLong = flag.String("language", "", "目标语言代码(--lang的长格式)")
|
||||||
configFile = flag.String("config", "", "配置文件路径")
|
configFile = flag.String("config", "", "配置文件路径")
|
||||||
@@ -38,8 +40,6 @@ var (
|
|||||||
interactiveShort = flag.Bool("i", false, "启动交互式翻译界面(-i的短格式)")
|
interactiveShort = flag.Bool("i", false, "启动交互式翻译界面(-i的短格式)")
|
||||||
)
|
)
|
||||||
|
|
||||||
const versionString = "YOYO翻译工具 v1.1.0"
|
|
||||||
|
|
||||||
// isPipeInput 检测是否有管道输入
|
// isPipeInput 检测是否有管道输入
|
||||||
func isPipeInput() bool {
|
func isPipeInput() bool {
|
||||||
fileInfo, err := os.Stdin.Stat()
|
fileInfo, err := os.Stdin.Stat()
|
||||||
@@ -104,6 +104,17 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *help || *h || *question {
|
||||||
|
printHelp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 -? 作为位置参数的情况
|
||||||
|
if flag.NArg() > 0 && (flag.Arg(0) == "-?" || flag.Arg(0) == "?") {
|
||||||
|
printHelp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 处理交互式模式
|
// 处理交互式模式
|
||||||
if *interactive || *interactiveShort || shouldStartInteractive() {
|
if *interactive || *interactiveShort || shouldStartInteractive() {
|
||||||
startInteractiveMode()
|
startInteractiveMode()
|
||||||
@@ -112,14 +123,8 @@ func main() {
|
|||||||
|
|
||||||
// 处理管道输入情况
|
// 处理管道输入情况
|
||||||
if isPipeInput() {
|
if isPipeInput() {
|
||||||
// 管道模式下,即使没有参数也继续执行
|
// 管道模式下,没有参数则显示帮助
|
||||||
if *help || *h {
|
if flag.NArg() == 0 {
|
||||||
printHelp()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 非管道模式下,没有参数则显示帮助
|
|
||||||
if *help || *h || flag.NArg() == 0 {
|
|
||||||
printHelp()
|
printHelp()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -372,12 +377,14 @@ func runCacheCommand(subcommand string) {
|
|||||||
|
|
||||||
// printVersion 显示版本信息
|
// printVersion 显示版本信息
|
||||||
func printVersion() {
|
func printVersion() {
|
||||||
fmt.Println(versionString)
|
logo.PrintLogoWithVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
// printHelp 显示帮助信息
|
// printHelp 显示帮助信息
|
||||||
func printHelp() {
|
func printHelp() {
|
||||||
fmt.Printf(`%s
|
logo.PrintLogoWithVersion()
|
||||||
|
|
||||||
|
fmt.Printf(`
|
||||||
|
|
||||||
使用方法:
|
使用方法:
|
||||||
yoyo [选项] <文本>
|
yoyo [选项] <文本>
|
||||||
@@ -445,8 +452,8 @@ func printHelp() {
|
|||||||
- 默认厂商: siliconflow
|
- 默认厂商: siliconflow
|
||||||
- 默认目标语言: zh-CN (简体中文)
|
- 默认目标语言: zh-CN (简体中文)
|
||||||
|
|
||||||
更多信息请访问: https://github.com/titor/fanyi
|
更多信息请访问: https://github.com/titor/fanyi
|
||||||
`, versionString)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// shouldStartInteractive 判断是否应该启动交互式模式
|
// shouldStartInteractive 判断是否应该启动交互式模式
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ type KeyMap struct {
|
|||||||
ScrollDown key.Binding
|
ScrollDown key.Binding
|
||||||
ScrollTop key.Binding
|
ScrollTop key.Binding
|
||||||
ScrollBottom key.Binding
|
ScrollBottom key.Binding
|
||||||
|
Help key.Binding
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewKeyMap() KeyMap {
|
func NewKeyMap() KeyMap {
|
||||||
@@ -44,5 +45,21 @@ func NewKeyMap() KeyMap {
|
|||||||
key.WithKeys("end"),
|
key.WithKeys("end"),
|
||||||
key.WithHelp("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"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"charm.land/bubbles/v2/help"
|
||||||
|
"charm.land/bubbles/v2/key"
|
||||||
"charm.land/bubbles/v2/spinner"
|
"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/logo"
|
||||||
"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
|
||||||
@@ -38,6 +35,7 @@ type model struct {
|
|||||||
input textarea.Model
|
input textarea.Model
|
||||||
viewport viewport.Model
|
viewport viewport.Model
|
||||||
spinner spinner.Model
|
spinner spinner.Model
|
||||||
|
help help.Model
|
||||||
keys KeyMap
|
keys KeyMap
|
||||||
|
|
||||||
targetLang string
|
targetLang string
|
||||||
@@ -72,16 +70,22 @@ func NewApp(cfg *config.Config, t *translator.Translator) *tea.Program {
|
|||||||
sp.Spinner = spinner.MiniDot
|
sp.Spinner = spinner.MiniDot
|
||||||
sp.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#8B5CF6"))
|
sp.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#8B5CF6"))
|
||||||
|
|
||||||
return tea.NewProgram(model{
|
hp := help.New()
|
||||||
|
|
||||||
|
m := 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,
|
spinner: sp,
|
||||||
|
help: hp,
|
||||||
keys: keys,
|
keys: keys,
|
||||||
targetLang: getDefaultLang(cfg),
|
targetLang: getDefaultLang(cfg),
|
||||||
})
|
}
|
||||||
|
|
||||||
|
p := tea.NewProgram(m)
|
||||||
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDefaultLang(cfg *config.Config) string {
|
func getDefaultLang(cfg *config.Config) string {
|
||||||
@@ -105,6 +109,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
m.width = msg.Width
|
m.width = msg.Width
|
||||||
m.height = msg.Height
|
m.height = msg.Height
|
||||||
|
m.help.SetWidth(msg.Width)
|
||||||
m.updateLayout()
|
m.updateLayout()
|
||||||
|
|
||||||
case translateMsg:
|
case translateMsg:
|
||||||
@@ -142,6 +147,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case "end":
|
case "end":
|
||||||
m.viewport.GotoBottom()
|
m.viewport.GotoBottom()
|
||||||
}
|
}
|
||||||
|
if key.Matches(msg, m.keys.Help) {
|
||||||
|
m.help.ShowAll = !m.help.ShowAll
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.input, cmd = m.input.Update(msg)
|
m.input, cmd = m.input.Update(msg)
|
||||||
@@ -211,7 +219,12 @@ func (m *model) updateLayout() {
|
|||||||
|
|
||||||
m.input.SetWidth(contentWidth)
|
m.input.SetWidth(contentWidth)
|
||||||
m.viewport.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 {
|
if m.viewport.Height() < 5 {
|
||||||
m.viewport.SetHeight(10)
|
m.viewport.SetHeight(10)
|
||||||
}
|
}
|
||||||
@@ -222,6 +235,10 @@ func (m *model) updateLayout() {
|
|||||||
func (m *model) updateViewportContent() {
|
func (m *model) updateViewportContent() {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
|
if len(m.messages) > 0 {
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
for _, msg := range m.messages {
|
for _, msg := range m.messages {
|
||||||
b.WriteString(m.renderTranslationCard(msg))
|
b.WriteString(m.renderTranslationCard(msg))
|
||||||
}
|
}
|
||||||
@@ -257,7 +274,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).
|
Padding(1, 3, 1, 3).
|
||||||
Width(m.viewport.Width()).
|
Width(m.viewport.Width()).
|
||||||
Render(outputContent)
|
Render(outputContent)
|
||||||
} else {
|
} else {
|
||||||
@@ -265,7 +282,7 @@ 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).
|
Padding(1, 3, 1, 3).
|
||||||
Width(m.viewport.Width()).
|
Width(m.viewport.Width()).
|
||||||
Render(outputContent)
|
Render(outputContent)
|
||||||
}
|
}
|
||||||
@@ -304,67 +321,26 @@ func (m model) View() tea.View {
|
|||||||
messages := m.viewport.View()
|
messages := m.viewport.View()
|
||||||
inputArea := m.renderInputArea()
|
inputArea := m.renderInputArea()
|
||||||
infoBar := m.renderInfoBar()
|
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 := 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 := gradientText(logoPattern, "#8B5CF6", "#EC4899")
|
title := logo.GradientText(logo.GetLogoPattern(), "#B413DC", "#00C8C8")
|
||||||
|
titleWithVersion := title + logo.GetVersionSuffix()
|
||||||
|
|
||||||
width := m.width - 4
|
width := m.width - 4
|
||||||
if width < 20 {
|
if width < 20 {
|
||||||
width = 60
|
width = 60
|
||||||
}
|
}
|
||||||
|
|
||||||
right := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#6B7280")).
|
|
||||||
Render("[Ctrl+C 退出]")
|
|
||||||
|
|
||||||
return lipgloss.NewStyle().
|
return lipgloss.NewStyle().
|
||||||
Width(width).
|
Width(width).
|
||||||
Render(title + strings.Repeat(" ", width-29-len(right)-1) + right)
|
Render(titleWithVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) renderInputArea() string {
|
func (m model) renderInputArea() string {
|
||||||
|
|||||||
41
memory.md
41
memory.md
@@ -895,3 +895,44 @@ v2:
|
|||||||
p := tea.NewProgram(model{})
|
p := tea.NewProgram(model{})
|
||||||
p.Run()
|
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`
|
||||||
91
taolun.md
91
taolun.md
@@ -832,3 +832,94 @@ config.GetUserEnvPath() // ~/.config/yoyo/.env
|
|||||||
- 分步表单 → `huh.NewForm(huh.NewGroup(...), huh.NewGroup(...))`
|
- 分步表单 → `huh.NewForm(huh.NewGroup(...), huh.NewGroup(...))`
|
||||||
|
|
||||||
**关联版本**: [changelog.md#1.1.0](changelog.md#110-2026-04-07)
|
**关联版本**: [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