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 }