Files
yoyo/internal/tui/app.go

245 lines
5.1 KiB
Go
Raw Normal View History

package tui
import (
"context"
"strings"
"github.com/charmbracelet/bubbles/textarea"
"github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/titor/fanyi/internal/config"
"github.com/titor/fanyi/internal/translator"
)
type model struct {
config *config.Config
translator *translator.Translator
textArea textarea.Model
result string
errMsg string
targetLang string
langIndex int
loading bool
width int
height int
}
type translateMsg struct {
result string
err error
}
var (
headerStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#00D9FF")).
Bold(true)
dividerStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#00D9FF"))
resultStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#98FB98")).
Background(lipgloss.Color("#0D1B2A"))
errorStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#FF6B6B")).
Background(lipgloss.Color("#1A1A2E"))
helpStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#888888"))
statusBarStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFFFF")).
Background(lipgloss.Color("#1F2937"))
langStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#FBBF24")).
Bold(true)
loadingStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#60A5FA"))
)
var supportedLangs = []string{"zh-CN", "en-US", "ja", "ko", "zh-TW", "es", "fr", "de"}
func NewApp(cfg *config.Config, t *translator.Translator) *tea.Program {
targetLang := "zh-CN"
if cfg != nil && cfg.DefaultTargetLang != "" {
targetLang = cfg.DefaultTargetLang
}
ta := textarea.New()
ta.Placeholder = "输入要翻译的文本..."
ta.Focus()
ta.Prompt = ""
ta.ShowLineNumbers = false
ta.SetHeight(3)
ta.FocusedStyle.Base = lipgloss.NewStyle().
Background(lipgloss.Color("#1A1A2E")).
Foreground(lipgloss.Color("#FAFAFA"))
return tea.NewProgram(model{
config: cfg,
translator: t,
textArea: ta,
targetLang: targetLang,
})
}
func (m model) Init() tea.Cmd {
return nil
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
m.updateTextAreaWidth()
case translateMsg:
m.loading = false
if msg.err != nil {
m.errMsg = msg.err.Error()
m.result = ""
} else {
m.result = msg.result
m.errMsg = ""
}
m.updateTextAreaHeight()
return m, nil
case tea.KeyMsg:
switch msg.Type {
case tea.KeyEnter:
if m.loading {
return m, nil
}
text := m.textArea.Value()
if text == "" {
return m, nil
}
m.loading = true
m.errMsg = ""
return m, m.doTranslate(text, m.targetLang)
case tea.KeyCtrlJ:
m.textArea, cmd = m.textArea.Update(msg)
m.updateTextAreaHeight()
return m, cmd
case tea.KeyCtrlC:
return m, tea.Quit
case tea.KeyCtrlL:
m.textArea.SetValue("")
m.result = ""
m.errMsg = ""
return m, nil
case tea.KeyCtrlT:
m.langIndex = (m.langIndex + 1) % len(supportedLangs)
m.targetLang = supportedLangs[m.langIndex]
return m, nil
case tea.KeyEsc:
return m, tea.Quit
}
m.textArea, cmd = m.textArea.Update(msg)
m.updateTextAreaHeight()
return m, cmd
default:
m.textArea, cmd = m.textArea.Update(msg)
m.updateTextAreaHeight()
}
return m, cmd
}
func (m model) updateTextAreaWidth() {
if m.width > 0 {
margin := 4
width := m.width - margin
if width < 20 {
width = 60
}
m.textArea.SetWidth(width)
}
}
func (m model) updateTextAreaHeight() {
lines := strings.Count(m.textArea.Value(), "\n") + 1
if lines < 1 {
lines = 1
}
if lines > 7 {
lines = 7
}
if lines < 3 {
lines = 3
}
m.textArea.SetHeight(lines)
}
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}
}
}
func (m model) View() string {
margin := " "
resultBox := m.renderResult()
return "\n" +
margin + headerStyle.Render("YOYO翻译") + "\n" +
margin + dividerStyle.Render(getDivider(m.width-2)) + "\n\n" +
margin + m.textArea.View() + "\n\n" +
margin + resultBox +
margin + dividerStyle.Render(getDivider(m.width-2)) + "\n" +
m.renderStatusBar()
}
func getDivider(width int) string {
if width < 10 {
width = 40
}
result := ""
for i := 0; i < width-4; i++ {
result += "─"
}
return result
}
func (m model) renderResult() string {
if m.loading {
return loadingStyle.Render("正在翻译...") + "\n\n"
}
if m.errMsg != "" {
return errorStyle.Render("错误: "+m.errMsg) + "\n\n"
}
if m.result == "" {
return helpStyle.Render("翻译结果将显示在这里...") + "\n\n"
}
return resultStyle.Render(m.result) + "\n\n"
}
func (m model) renderStatusBar() string {
width := m.width - 4
if width < 30 {
width = 60
}
langInfo := langStyle.Render("目标: " + m.targetLang)
hint := helpStyle.Render("按 / 显示命令")
return " " + statusBarStyle.Render(" "+langInfo+" ") + " " + hint
}