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 msg.Alt { m.textArea.InsertString("\n") m.updateTextAreaHeight() return m, nil } 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.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 }