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

190 lines
5.5 KiB
Go

package internal
import (
"fmt"
"strings"
"charm.land/lipgloss/v2"
"u-tabs/internal/style"
)
// --- Workspace 三栏渲染 ---
func (m *Model) renderWorkspace() string {
g := Groups[m.activeGroup]
svcs := WorkspacesByGroup(g.Label)
if len(svcs) == 0 {
return style.SubtitleStyle.Render(" empty")
}
avail := m.width - 8
tabW := max(14, min(20, avail*18/100))
listW := max(30, avail*45/100)
detailW := avail - tabW - listW
tabCw := tabW - 2
listCw := listW - 2
detailCw := detailW - 2
listH := max(3, m.height-7)
tabLines := m.tabsColumnLines(tabCw, listH-1)
svcLines := m.wsListLines(g, svcs, listCw, listH-1)
detailLines := m.wsDetailLines(svcs, detailCw, listH-1)
maxRows := min(listH-1, max(max(len(tabLines), len(svcLines)), len(detailLines)))
tabBorderFg := style.BgPanel
if m.wsFocus == 0 {
tabBorderFg = style.Accent
}
listBorderFg := style.BgPanel
if m.wsFocus == 1 {
listBorderFg = style.Accent
}
panelSty := lipgloss.NewStyle().Border(lipgloss.RoundedBorder())
tabBox := panelSty.BorderForeground(tabBorderFg).Width(tabW).Render(
style.DetailTitle.Render("tabs") + "\n" + padLinesTo(tabLines, tabCw, maxRows))
listBox := panelSty.BorderForeground(listBorderFg).Width(listW).Render(
style.DetailTitle.Render(g.Label) + "\n" + padLinesTo(svcLines, listCw, maxRows))
detailBox := panelSty.BorderForeground(style.BgPanel).Width(detailW).Render(
style.DetailTitle.Render("detail") + "\n" + padLinesTo(detailLines, detailCw, maxRows))
return lipgloss.JoinHorizontal(lipgloss.Top, tabBox, " ", listBox, " ", detailBox)
}
func (m *Model) tabsColumnLines(w, listH int) []string {
var lines []string
total := len(Groups) + 1
start, end := viewport(m.wsTabCur, total, listH)
focused := m.wsFocus == 0
for i := start; i < end; i++ {
if i == len(Groups) {
renderNavEntry(&lines, w, "历史对话", "#7dcfff", m.wsTabCur == i, focused)
continue
}
g := Groups[i]
label := g.Label
gs := style.GroupStyles[g.Label]
isActive := m.wsTabCur == i
if isActive {
line := " ▸ " + label
line = padRightByWidth(truncateByWidth(line, w), w)
if focused && gs.GetForeground() != (lipgloss.Color("")) {
sty := lipgloss.NewStyle().Foreground(style.BgDark).Background(gs.GetForeground()).Bold(true)
lines = append(lines, sty.Render(line))
} else if gs.GetForeground() != (lipgloss.Color("")) {
sty := lipgloss.NewStyle().Foreground(gs.GetForeground()).Bold(true)
lines = append(lines, sty.Render(line))
} else {
lines = append(lines, style.SelStyle.Render(line))
}
} else {
line := " " + label
line = padRightByWidth(truncateByWidth(line, w), w)
if gs.GetForeground() != (lipgloss.Color("")) {
lines = append(lines, gs.Render(line))
} else {
lines = append(lines, style.NormStyle.Render(line))
}
}
}
return lines
}
func (m *Model) wsListLines(g Group, svcs []Workspace, w, listH int) []string {
start, end := viewport(m.cursor, len(svcs), listH)
maxTitleW := 0
for _, ws := range svcs {
if tw := stringWidth(ws.Title); tw > maxTitleW {
maxTitleW = tw
}
}
var lines []string
for i := start; i < end; i++ {
ws := svcs[i]
cur := " "
if i == m.cursor {
cur = "▸"
}
mark := " "
if m.selected[ws.Index] {
mark = "✓"
}
paddedTitle := padRightByWidth(ws.Title, maxTitleW)
if i == m.cursor {
prefix := cur + " " + mark + " " + fmt.Sprintf("%02d", ws.N) + " "
remainW := max(10, w-stringWidth(prefix))
text := truncateByWidth(paddedTitle+" "+ws.Prompt, remainW)
sty := style.SelStyle
if m.wsFocus != 1 {
sty = lipgloss.NewStyle().Foreground(style.Accent).Bold(true)
}
lines = append(lines, sty.Render(padRightByWidth(truncateByWidth(prefix+text, w), w)))
} else {
markStr := " "
if m.selected[ws.Index] {
markStr = style.MarkStyle.Render("✓")
}
num := style.NumStyle.Render(fmt.Sprintf("%02d", ws.N))
prefix := cur + " " + markStr + " " + num + " "
remainW := max(10, w-stringWidth(prefix))
text := truncateByWidth(paddedTitle+" "+ws.Prompt, remainW)
lines = append(lines, style.NormStyle.Render(padRightByWidth(truncateByWidth(prefix+text, w), w)))
}
}
return lines
}
func (m *Model) wsDetailLines(svcs []Workspace, w, listH int) []string {
if m.cursor >= len(svcs) {
return []string{lipgloss.NewStyle().Foreground(style.Dim).Render(fitWidth(" ← select to view", w))}
}
ws := svcs[m.cursor]
sepSty := lipgloss.NewStyle().Foreground(style.BgPanel)
var lines []string
lines = append(lines, style.DetailTitle.Render(fitWidth(" "+ws.Title, w)))
lines = append(lines, sepSty.Render(fitWidth(strings.Repeat("─", w), w)))
rows := []struct {
key string
val string
sty lipgloss.Style
}{
{"dir", ws.Dir, style.ValStyle},
{"no", fmt.Sprintf("%02d · %s", ws.N, ws.Group), style.NumStyle},
{"desc", ws.Prompt, style.ValStyle},
{"tech", ws.Tech, style.TechStyle},
{"deploy", ws.Deploy, style.DeployStyle},
}
for _, r := range rows {
line := fmt.Sprintf(" %-6s %s", r.key, r.sty.Render(r.val))
lines = append(lines, style.NormStyle.Render(fitWidth(line, w)))
}
lines = append(lines, sepSty.Render(fitWidth(strings.Repeat("─", w), w)))
hintCmd := "wt"
if !isWindows {
hintCmd = "bash"
}
lines = append(lines, lipgloss.NewStyle().Foreground(style.Accent).Italic(true).Render(fitWidth(" $ "+hintCmd+" → CC @"+ws.Title, w)))
lines = append(lines, lipgloss.NewStyle().Foreground(style.Dim).Render(fitWidth(" [Enter]start [Space]multi", w)))
if len(lines) > listH {
lines = lines[:listH]
}
return lines
}