Files
u-tabs/internal/view.go
绝尘 36aeef4bb7 新增: 会话分叉功能,优化增量扫描缓存
- 会话分叉: 按 c 键从历史会话分叉,支持带方向提示
- 启动自动加载历史记录
- 增量扫描: 缓存内存化、目录 modTime 跳过、已删除条目裁剪
- 刷新按钮复用 onTabSwitch 单一入口
2026-05-31 21:40:20 +08:00

156 lines
4.3 KiB
Go

package internal
import (
"fmt"
"strings"
"charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"u-tabs/internal/style"
)
// --- 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(lipgloss.NewStyle().Foreground(style.BgPanel).Render(strings.Repeat("─", 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(lipgloss.NewStyle().Foreground(style.Accent).Render(fmt.Sprintf(" ✓ %s", m.launched)))
m.launched = ""
}
if m.update.Done {
b.WriteString(lipgloss.NewStyle().Foreground(style.Success).Bold(true).
Render(fmt.Sprintf(" updated to v%s, restart to apply", m.update.NewVersion)))
} else if m.update.Error != nil {
b.WriteString(lipgloss.NewStyle().Foreground(style.Red).
Render(fmt.Sprintf(" update failed: %v", m.update.Error)))
} else if m.update.Updating {
b.WriteString(lipgloss.NewStyle().Foreground(style.Warning).
Render(" updating..."))
} else if m.update.Available {
b.WriteString(lipgloss.NewStyle().Foreground(style.Warning).Bold(true).
Render(fmt.Sprintf(" v%s available [u]update", m.update.NewVersion)))
}
b.WriteString("\n")
helpParts := m.renderHelp()
helpStr := " " + strings.Join(helpParts, " ")
verStr := lipgloss.NewStyle().Foreground(style.Dim).Render("绝尘 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(lipgloss.NewStyle().
Bold(true).Background(style.BgPanel).Foreground(style.Accent).
Render(wsLabel))
} else {
b.WriteString(style.TabInactiveStyle.Render(wsLabel))
}
b.WriteString(sep)
histLabel := " 2 历史对话 "
if m.IsHistoryTab() {
b.WriteString(lipgloss.NewStyle().
Bold(true).Background(style.BgPanel).Foreground(style.Cyan).
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("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 lipgloss.NewStyle().Foreground(style.Accent).Render(key) +
lipgloss.NewStyle().Foreground(style.Dim).Render(":" + desc)
}
func renderNavEntry(lines *[]string, w int, label string, activeFg string, isCur, focused bool) {
sep := lipgloss.NewStyle().Foreground(style.BgPanel).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, lipgloss.NewStyle().Foreground(style.Dim).Render(line))
}
}