- 会话分叉: 按 c 键从历史会话分叉,支持带方向提示 - 启动自动加载历史记录 - 增量扫描: 缓存内存化、目录 modTime 跳过、已删除条目裁剪 - 刷新按钮复用 onTabSwitch 单一入口
190 lines
5.5 KiB
Go
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
|
|
}
|