- 标题自定义编辑:HISTORY 详情按 e 编辑标题,UserTitle 持久化到缓存且最高优先级 - AI 摘要质量修复:困惑回复检测词表收紧,消息提取过滤噪音,修复 JSON 包裹困惑回复漏网 - 数据维护命令:--fix-titles 清理低质量摘要,--regen-summaries 并发生成 - 修复代码审查 9 个问题:退格 UTF-8 损坏、中文输入、flush 调度、误杀合法摘要、 按键全局拦截、UserTitle 未同步窗口标题、wt 启动回落、cmd 注入防护、前缀误隐藏会话 - 启动链 wt.exe 回退与 cmd start 标题转义 - gofmt 规范化,新增 launch_test.go
154 lines
3.9 KiB
Go
154 lines
3.9 KiB
Go
package internal
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"u-tabs/internal/style"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
"charm.land/lipgloss/v2"
|
|
)
|
|
|
|
// --- View 入口 ---
|
|
|
|
func (m *Model) View() tea.View {
|
|
v := tea.NewView(m.render())
|
|
v.AltScreen = true
|
|
v.MouseMode = tea.MouseModeCellMotion
|
|
v.WindowTitle = "U-TABS"
|
|
return v
|
|
}
|
|
|
|
func (m *Model) render() string {
|
|
if m.width == 0 {
|
|
return "loading..."
|
|
}
|
|
|
|
var b strings.Builder
|
|
|
|
b.WriteString(m.renderTabBar())
|
|
b.WriteString("\n")
|
|
b.WriteString(style.SepFg.Render(sepRunes(m.width)))
|
|
b.WriteString("\n")
|
|
|
|
if m.IsHistoryTab() {
|
|
b.WriteString(m.renderHistory())
|
|
} else {
|
|
b.WriteString(m.renderWorkspace())
|
|
}
|
|
|
|
// footer
|
|
b.WriteString("\n")
|
|
if m.inputBuf != "" {
|
|
b.WriteString(style.InputStyle.Render(fmt.Sprintf(" ▶ num:%s [Enter]go [Esc]cancel", m.inputBuf)))
|
|
}
|
|
if m.forkMode {
|
|
b.WriteString(style.InputStyle.Render(fmt.Sprintf(" ⎇ fork:%s [Enter]confirm [Esc]cancel", m.forkBuf)))
|
|
}
|
|
if m.launched != "" {
|
|
b.WriteString(style.AccentFg.Render(fmt.Sprintf(" ✓ %s", m.launched)))
|
|
m.launched = ""
|
|
}
|
|
if m.update.Done {
|
|
b.WriteString(style.SuccessBoldFg.
|
|
Render(fmt.Sprintf(" updated to v%s, restart to apply", m.update.NewVersion)))
|
|
} else if m.update.Error != nil {
|
|
b.WriteString(style.RedFg.
|
|
Render(fmt.Sprintf(" update failed: %v", m.update.Error)))
|
|
} else if m.update.Updating {
|
|
b.WriteString(style.WarningFg.
|
|
Render(" updating..."))
|
|
} else if m.update.Available {
|
|
b.WriteString(style.WarningBoldFg.
|
|
Render(fmt.Sprintf(" v%s available [u]update", m.update.NewVersion)))
|
|
}
|
|
b.WriteString("\n")
|
|
|
|
helpParts := m.renderHelp()
|
|
helpStr := " " + strings.Join(helpParts, " ")
|
|
verStr := style.DimFg.Render("U-TABS v" + Version)
|
|
padW := m.width - lipgloss.Width(helpStr) - lipgloss.Width(verStr)
|
|
if padW > 0 {
|
|
b.WriteString(helpStr + strings.Repeat(" ", padW) + verStr)
|
|
} else {
|
|
b.WriteString(helpStr + " " + verStr)
|
|
}
|
|
|
|
if hint := ConfigHint(); hint != "" {
|
|
b.WriteString("\n" + hint)
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// renderTabBar 顶部两个主 tab
|
|
func (m *Model) renderTabBar() string {
|
|
var b strings.Builder
|
|
b.WriteString(style.TitleStyle.Render(" U-TABS "))
|
|
sep := style.TabSep.Render(" │ ")
|
|
|
|
wsLabel := " 1 工作空间 "
|
|
if !m.IsHistoryTab() {
|
|
b.WriteString(style.TabActiveAccent.Render(wsLabel))
|
|
} else {
|
|
b.WriteString(style.TabInactiveStyle.Render(wsLabel))
|
|
}
|
|
|
|
b.WriteString(sep)
|
|
histLabel := " 2 历史对话 "
|
|
if m.IsHistoryTab() {
|
|
b.WriteString(style.TabActiveCyan.Render(histLabel))
|
|
} else {
|
|
b.WriteString(style.TabInactiveStyle.Render(histLabel))
|
|
}
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func (m *Model) renderHelp() []string {
|
|
if m.IsHistoryTab() {
|
|
return []string{
|
|
m.fmtHelp("↑↓", "sel"),
|
|
m.fmtHelp("←→", "panel"),
|
|
m.fmtHelp("Enter", "resume"),
|
|
m.fmtHelp("f", "star"),
|
|
m.fmtHelp("e", "rename"),
|
|
m.fmtHelp("n", "new"),
|
|
m.fmtHelp("c", "fork"),
|
|
m.fmtHelp("1", "workspace"),
|
|
m.fmtHelp("r/F5", "refresh"),
|
|
m.fmtHelp("q", "quit"),
|
|
}
|
|
}
|
|
return []string{
|
|
m.fmtHelp("←→", "col"),
|
|
m.fmtHelp("↑↓", "sel"),
|
|
m.fmtHelp("Enter", "run"),
|
|
m.fmtHelp("2", "history"),
|
|
m.fmtHelp("q", "quit"),
|
|
}
|
|
}
|
|
|
|
func (m *Model) fmtHelp(key, desc string) string {
|
|
return style.AccentFg.Render(key) +
|
|
style.DimFg.Render(":"+desc)
|
|
}
|
|
|
|
func renderNavEntry(lines *[]string, w int, label string, activeFg string, isCur, focused bool) {
|
|
sep := style.SepFg.Render(fitWidth(" ─────────", w))
|
|
*lines = append(*lines, sep)
|
|
if isCur {
|
|
curLabel := " → " + label
|
|
line := padRightByWidth(truncateByWidth(curLabel, w), w)
|
|
if focused {
|
|
*lines = append(*lines, style.SelStyle.Render(line))
|
|
} else {
|
|
*lines = append(*lines, lipgloss.NewStyle().Foreground(lipgloss.Color(activeFg)).Bold(true).Render(line))
|
|
}
|
|
} else {
|
|
inactiveLabel := " " + label
|
|
line := padRightByWidth(truncateByWidth(inactiveLabel, w), w)
|
|
*lines = append(*lines, style.DimFg.Render(line))
|
|
}
|
|
}
|