2026-04-06 05:07:36 +08:00
|
|
|
package tui
|
|
|
|
|
|
|
|
|
|
import (
|
2026-04-06 05:18:11 +08:00
|
|
|
"context"
|
|
|
|
|
|
2026-04-06 05:10:00 +08:00
|
|
|
"github.com/charmbracelet/bubbles/textinput"
|
2026-04-06 05:07:36 +08:00
|
|
|
"github.com/charmbracelet/bubbletea"
|
2026-04-06 05:10:00 +08:00
|
|
|
"github.com/charmbracelet/lipgloss"
|
2026-04-06 05:07:36 +08:00
|
|
|
"github.com/titor/fanyi/internal/config"
|
|
|
|
|
"github.com/titor/fanyi/internal/translator"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type model struct {
|
|
|
|
|
config *config.Config
|
|
|
|
|
translator *translator.Translator
|
2026-04-06 05:10:00 +08:00
|
|
|
textInput textinput.Model
|
2026-04-06 05:11:23 +08:00
|
|
|
result string
|
2026-04-06 05:18:11 +08:00
|
|
|
errMsg string
|
2026-04-06 05:13:24 +08:00
|
|
|
targetLang string
|
2026-04-06 05:15:24 +08:00
|
|
|
langIndex int
|
2026-04-06 05:18:11 +08:00
|
|
|
loading bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type translateMsg struct {
|
|
|
|
|
result string
|
|
|
|
|
err error
|
2026-04-06 05:07:36 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-06 05:10:00 +08:00
|
|
|
var (
|
2026-04-06 05:11:23 +08:00
|
|
|
headerStyle = lipgloss.NewStyle().
|
|
|
|
|
Foreground(lipgloss.Color("#00D9FF")).
|
|
|
|
|
Bold(true)
|
2026-04-06 05:13:24 +08:00
|
|
|
dividerStyle = lipgloss.NewStyle().
|
|
|
|
|
Foreground(lipgloss.Color("#00D9FF"))
|
2026-04-06 05:10:00 +08:00
|
|
|
inputStyle = lipgloss.NewStyle().
|
|
|
|
|
Foreground(lipgloss.Color("#FAFAFA")).
|
|
|
|
|
Background(lipgloss.Color("#1A1A2E"))
|
2026-04-06 05:11:23 +08:00
|
|
|
resultStyle = lipgloss.NewStyle().
|
|
|
|
|
Foreground(lipgloss.Color("#98FB98")).
|
|
|
|
|
Background(lipgloss.Color("#0D1B2A"))
|
2026-04-06 05:18:11 +08:00
|
|
|
errorStyle = lipgloss.NewStyle().
|
|
|
|
|
Foreground(lipgloss.Color("#FF6B6B")).
|
|
|
|
|
Background(lipgloss.Color("#1A1A2E"))
|
2026-04-06 05:11:23 +08:00
|
|
|
helpStyle = lipgloss.NewStyle().
|
|
|
|
|
Foreground(lipgloss.Color("#888888"))
|
2026-04-06 05:13:24 +08:00
|
|
|
statusBarStyle = lipgloss.NewStyle().
|
|
|
|
|
Foreground(lipgloss.Color("#FFFFFF")).
|
|
|
|
|
Background(lipgloss.Color("#1F2937")).
|
|
|
|
|
Width(60)
|
|
|
|
|
langStyle = lipgloss.NewStyle().
|
|
|
|
|
Foreground(lipgloss.Color("#FBBF24")).
|
|
|
|
|
Bold(true)
|
2026-04-06 05:15:24 +08:00
|
|
|
keyStyle = lipgloss.NewStyle().
|
|
|
|
|
Foreground(lipgloss.Color("#60A5FA"))
|
2026-04-06 05:18:11 +08:00
|
|
|
loadingStyle = lipgloss.NewStyle().
|
|
|
|
|
Foreground(lipgloss.Color("#60A5FA"))
|
2026-04-06 05:10:00 +08:00
|
|
|
)
|
|
|
|
|
|
2026-04-06 05:15:24 +08:00
|
|
|
var supportedLangs = []string{"zh-CN", "en-US", "ja", "ko", "zh-TW", "es", "fr", "de"}
|
|
|
|
|
|
2026-04-06 05:07:36 +08:00
|
|
|
func NewApp(cfg *config.Config, t *translator.Translator) *tea.Program {
|
2026-04-06 05:13:24 +08:00
|
|
|
targetLang := "zh-CN"
|
|
|
|
|
if cfg != nil && cfg.DefaultTargetLang != "" {
|
|
|
|
|
targetLang = cfg.DefaultTargetLang
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 05:10:00 +08:00
|
|
|
ti := textinput.New()
|
|
|
|
|
ti.Placeholder = "输入要翻译的文本..."
|
|
|
|
|
ti.Focus()
|
|
|
|
|
ti.Prompt = "> "
|
|
|
|
|
ti.TextStyle = inputStyle
|
|
|
|
|
|
2026-04-06 05:07:36 +08:00
|
|
|
return tea.NewProgram(model{
|
|
|
|
|
config: cfg,
|
|
|
|
|
translator: t,
|
2026-04-06 05:10:00 +08:00
|
|
|
textInput: ti,
|
2026-04-06 05:13:24 +08:00
|
|
|
targetLang: targetLang,
|
2026-04-06 05:07:36 +08:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m model) Init() tea.Cmd {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
2026-04-06 05:10:00 +08:00
|
|
|
var cmd tea.Cmd
|
|
|
|
|
|
|
|
|
|
switch msg := msg.(type) {
|
2026-04-06 05:18:11 +08:00
|
|
|
case translateMsg:
|
|
|
|
|
m.loading = false
|
|
|
|
|
if msg.err != nil {
|
|
|
|
|
m.errMsg = msg.err.Error()
|
|
|
|
|
m.result = ""
|
|
|
|
|
} else {
|
|
|
|
|
m.result = msg.result
|
|
|
|
|
m.errMsg = ""
|
|
|
|
|
}
|
|
|
|
|
return m, nil
|
|
|
|
|
|
2026-04-06 05:10:00 +08:00
|
|
|
case tea.KeyMsg:
|
|
|
|
|
switch msg.Type {
|
|
|
|
|
case tea.KeyEnter:
|
2026-04-06 05:18:11 +08:00
|
|
|
if m.loading {
|
|
|
|
|
return m, nil
|
|
|
|
|
}
|
|
|
|
|
text := m.textInput.Value()
|
|
|
|
|
if text == "" {
|
|
|
|
|
return m, nil
|
|
|
|
|
}
|
|
|
|
|
m.loading = true
|
|
|
|
|
m.errMsg = ""
|
|
|
|
|
return m, m.doTranslate(text, m.targetLang)
|
|
|
|
|
|
2026-04-06 05:15:24 +08:00
|
|
|
case tea.KeyCtrlC:
|
|
|
|
|
return m, tea.Quit
|
2026-04-06 05:18:11 +08:00
|
|
|
|
2026-04-06 05:15:24 +08:00
|
|
|
case tea.KeyCtrlL:
|
|
|
|
|
m.textInput.SetValue("")
|
|
|
|
|
m.result = ""
|
2026-04-06 05:18:11 +08:00
|
|
|
m.errMsg = ""
|
2026-04-06 05:15:24 +08:00
|
|
|
return m, nil
|
2026-04-06 05:18:11 +08:00
|
|
|
|
2026-04-06 05:15:24 +08:00
|
|
|
case tea.KeyCtrlT:
|
|
|
|
|
m.langIndex = (m.langIndex + 1) % len(supportedLangs)
|
|
|
|
|
m.targetLang = supportedLangs[m.langIndex]
|
|
|
|
|
return m, nil
|
2026-04-06 05:18:11 +08:00
|
|
|
|
2026-04-06 05:15:24 +08:00
|
|
|
case tea.KeyEsc:
|
2026-04-06 05:10:00 +08:00
|
|
|
return m, tea.Quit
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m.textInput, cmd = m.textInput.Update(msg)
|
|
|
|
|
return m, cmd
|
2026-04-06 05:07:36 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-06 05:18:11 +08:00
|
|
|
func (m model) doTranslate(text, toLang string) tea.Cmd {
|
|
|
|
|
return func() tea.Msg {
|
|
|
|
|
result, err := m.translator.Translate(
|
|
|
|
|
context.Background(),
|
|
|
|
|
text,
|
|
|
|
|
&translator.TranslateOptions{
|
|
|
|
|
ToLang: toLang,
|
|
|
|
|
PromptName: "simple",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return translateMsg{err: err}
|
|
|
|
|
}
|
|
|
|
|
return translateMsg{result: result.Translated}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 05:07:36 +08:00
|
|
|
func (m model) View() string {
|
2026-04-06 05:11:23 +08:00
|
|
|
resultBox := m.renderResult()
|
2026-04-06 05:15:24 +08:00
|
|
|
helpText := helpStyle.Render("\n " +
|
|
|
|
|
keyStyle.Render("Ctrl+L") + " 清空 " +
|
|
|
|
|
keyStyle.Render("Ctrl+T") + " 切换语言 " +
|
|
|
|
|
keyStyle.Render("Enter") + " 翻译 " +
|
|
|
|
|
keyStyle.Render("Ctrl+C") + " 退出")
|
2026-04-06 05:11:23 +08:00
|
|
|
|
2026-04-06 05:10:00 +08:00
|
|
|
return "\n" +
|
2026-04-06 05:11:23 +08:00
|
|
|
" " + headerStyle.Render("YOYO翻译") + "\n" +
|
2026-04-06 05:13:24 +08:00
|
|
|
" " + dividerStyle.Render("─────────────────────") + "\n\n" +
|
2026-04-06 05:10:00 +08:00
|
|
|
" " + m.textInput.View() + "\n\n" +
|
2026-04-06 05:11:23 +08:00
|
|
|
resultBox +
|
2026-04-06 05:13:24 +08:00
|
|
|
helpText +
|
|
|
|
|
"\n" +
|
|
|
|
|
m.renderStatusBar()
|
2026-04-06 05:11:23 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m model) renderResult() string {
|
2026-04-06 05:18:11 +08:00
|
|
|
if m.loading {
|
|
|
|
|
return " " + loadingStyle.Render("正在翻译...") + "\n"
|
|
|
|
|
}
|
|
|
|
|
if m.errMsg != "" {
|
|
|
|
|
return " " + errorStyle.Render("错误: "+m.errMsg) + "\n"
|
|
|
|
|
}
|
2026-04-06 05:11:23 +08:00
|
|
|
if m.result == "" {
|
|
|
|
|
return " " + helpStyle.Render("翻译结果将显示在这里...") + "\n"
|
|
|
|
|
}
|
|
|
|
|
return " " + resultStyle.Render(m.result) + "\n"
|
2026-04-06 05:07:36 +08:00
|
|
|
}
|
2026-04-06 05:13:24 +08:00
|
|
|
|
|
|
|
|
func (m model) renderStatusBar() string {
|
|
|
|
|
divider := dividerStyle.Render("─")
|
|
|
|
|
langInfo := langStyle.Render("目标: " + m.targetLang)
|
|
|
|
|
|
|
|
|
|
return "\n " + divider + "\n" +
|
|
|
|
|
" " + statusBarStyle.Render(" "+langInfo+" ")
|
|
|
|
|
}
|