- 会话分叉: 按 c 键从历史会话分叉,支持带方向提示 - 启动自动加载历史记录 - 增量扫描: 缓存内存化、目录 modTime 跳过、已删除条目裁剪 - 刷新按钮复用 onTabSwitch 单一入口
256 lines
7.1 KiB
Go
256 lines
7.1 KiB
Go
package internal
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"charm.land/lipgloss/v2"
|
|
"u-tabs/internal/style"
|
|
)
|
|
|
|
// --- HISTORY 三栏渲染 ---
|
|
|
|
func (m *Model) renderHistory() string {
|
|
if !m.history.Loaded && !m.history.Scanning {
|
|
return style.ScanningStyle.Render(" press Tab to load sessions...")
|
|
}
|
|
if len(m.history.Projects) == 0 && m.history.Scanning {
|
|
return style.ScanningStyle.Render(" scanning sessions...")
|
|
}
|
|
if len(m.history.Projects) == 0 {
|
|
return style.SubtitleStyle.Render(" no sessions found")
|
|
}
|
|
|
|
avail := m.width - 8
|
|
dirW := max(18, avail*20/100)
|
|
sessW := max(28, avail*40/100)
|
|
detailW := avail - dirW - sessW
|
|
listH := max(3, m.height-7)
|
|
|
|
dirCw := dirW - 2
|
|
sessCw := sessW - 2
|
|
detailCw := detailW - 2
|
|
|
|
dirLines := m.dirColumnLines(dirCw, listH-1)
|
|
sessLines := m.sessColumnLines(sessCw, listH-1)
|
|
detailLines := m.detailColumnLines(detailCw, listH-1)
|
|
|
|
maxRows := min(listH-1, max(max(len(dirLines), len(sessLines)), len(detailLines)))
|
|
|
|
panelSty := lipgloss.NewStyle().
|
|
Border(lipgloss.RoundedBorder()).
|
|
BorderForeground(style.BgPanel)
|
|
|
|
dirBox := panelSty.Width(dirW).Render(
|
|
style.DetailTitle.Render("dirs") + "\n" + padLinesTo(dirLines, dirCw, maxRows))
|
|
sessBox := panelSty.Width(sessW).Render(
|
|
style.DetailTitle.Render("sessions") + "\n" + padLinesTo(sessLines, sessCw, maxRows))
|
|
detailBox := panelSty.Width(detailW).Render(
|
|
style.DetailTitle.Render("detail") + "\n" + padLinesTo(detailLines, detailCw, maxRows))
|
|
|
|
return lipgloss.JoinHorizontal(lipgloss.Top, dirBox, " ", sessBox, " ", detailBox)
|
|
}
|
|
|
|
func (m *Model) dirColumnLines(w, listH int) []string {
|
|
var lines []string
|
|
total := len(m.history.Projects) + 3
|
|
start, end := viewport(m.history.DirCursor, total, listH)
|
|
|
|
prefixW := 3
|
|
|
|
buildDirLine := func(cur, name, cnt string, selected bool) string {
|
|
cntW := stringWidth(cnt)
|
|
nameW := max(2, w-prefixW-cntW)
|
|
name = truncateByWidth(name, nameW)
|
|
line := " " + cur + " " + padRightByWidth(name, nameW) + cnt
|
|
sty := style.NormStyle
|
|
if selected {
|
|
sty = style.SelStyle
|
|
}
|
|
return sty.Render(padRightByWidth(truncateByWidth(line, w), w))
|
|
}
|
|
|
|
// 0: 全部
|
|
if start == 0 {
|
|
totalSess := 0
|
|
for _, pd := range m.history.Projects {
|
|
totalSess += len(pd.Sessions)
|
|
}
|
|
cur := " "
|
|
if m.history.DirCursor == 0 {
|
|
cur = "▸"
|
|
}
|
|
lines = append(lines, buildDirLine(cur, "全部", fmt.Sprintf("(%d)", totalSess),
|
|
m.history.DirCursor == 0 && m.history.FocusPanel == 0))
|
|
}
|
|
|
|
// 1: 收藏
|
|
if start <= 1 && end > 1 {
|
|
favCnt := countFavorites(m.history.Projects, m.history.Favorites)
|
|
cur := " "
|
|
if m.history.DirCursor == 1 {
|
|
cur = "▸"
|
|
}
|
|
lines = append(lines, buildDirLine(cur, "★ 收藏", fmt.Sprintf("(%d)", favCnt),
|
|
m.history.DirCursor == 1 && m.history.FocusPanel == 0))
|
|
}
|
|
|
|
// 2..N+1: 项目目录
|
|
for i := 0; i < len(m.history.Projects); i++ {
|
|
idx := i + 2
|
|
if idx < start || idx >= end {
|
|
continue
|
|
}
|
|
pd := m.history.Projects[i]
|
|
cur := " "
|
|
if m.history.DirCursor == idx {
|
|
cur = "▸"
|
|
}
|
|
lines = append(lines, buildDirLine(cur, pd.DirShort, fmt.Sprintf("(%d)", len(pd.Sessions)),
|
|
m.history.DirCursor == idx && m.history.FocusPanel == 0))
|
|
}
|
|
|
|
// N+2: 回到工作空间入口
|
|
backIdx := len(m.history.Projects) + 2
|
|
if end > backIdx {
|
|
renderNavEntry(&lines, w, "工作空间", "#7aa2f7", m.history.DirCursor == backIdx, m.history.FocusPanel == 0)
|
|
}
|
|
return lines
|
|
}
|
|
|
|
func (m *Model) sessColumnLines(w, listH int) []string {
|
|
sessions := m.currentSessions()
|
|
if len(sessions) == 0 {
|
|
return []string{style.SubtitleStyle.Render(fitWidth(" no sessions", w))}
|
|
}
|
|
|
|
prefixW := 16
|
|
textW := max(8, w-prefixW)
|
|
|
|
type sessBlock struct {
|
|
lines []string
|
|
}
|
|
var blocks []sessBlock
|
|
totalLines := 0
|
|
curLineOffset := 0
|
|
|
|
for i, s := range sessions {
|
|
cur := " "
|
|
if i == m.history.SessCursor {
|
|
cur = "▸ "
|
|
}
|
|
timeStr := s.StartTime.Local().Format("01-02 15:04")
|
|
favMark := " "
|
|
if m.history.Favorites[s.ID] {
|
|
favMark = "★"
|
|
}
|
|
title := s.DisplayTitle()
|
|
|
|
wrapped := wrapByWidth(title, textW)
|
|
if len(wrapped) == 0 {
|
|
wrapped = []string{""}
|
|
}
|
|
|
|
sty := style.NormStyle
|
|
if i == m.history.SessCursor && m.history.FocusPanel == 1 {
|
|
sty = style.SelStyle
|
|
}
|
|
|
|
var blockLines []string
|
|
for li, part := range wrapped {
|
|
var line string
|
|
if li == 0 {
|
|
line = fmt.Sprintf(" %s%s%s %s", cur, timeStr, favMark, part)
|
|
} else {
|
|
line = strings.Repeat(" ", prefixW+1) + part
|
|
}
|
|
blockLines = append(blockLines, sty.Render(padRightByWidth(truncateByWidth(line, w), w)))
|
|
}
|
|
|
|
if i == m.history.SessCursor {
|
|
curLineOffset = totalLines
|
|
}
|
|
totalLines += len(blockLines)
|
|
blocks = append(blocks, sessBlock{lines: blockLines})
|
|
}
|
|
|
|
startLine, endLine := viewport(curLineOffset, totalLines, listH)
|
|
|
|
var allLines []string
|
|
for _, b := range blocks {
|
|
allLines = append(allLines, b.lines...)
|
|
}
|
|
if endLine > len(allLines) {
|
|
endLine = len(allLines)
|
|
}
|
|
return allLines[startLine:endLine]
|
|
}
|
|
|
|
func (m *Model) detailColumnLines(w, listH int) []string {
|
|
s := m.currentSession()
|
|
if s == nil {
|
|
return []string{lipgloss.NewStyle().Foreground(style.Dim).Render(fitWidth(" ← select to view", w))}
|
|
}
|
|
|
|
sepSty := lipgloss.NewStyle().Foreground(style.BgPanel)
|
|
var lines []string
|
|
|
|
favPrefix := " "
|
|
if m.history.Favorites[s.ID] {
|
|
favPrefix = "★ "
|
|
}
|
|
lines = append(lines, style.DetailTitle.Render(fitWidth(favPrefix+s.DisplayTitle(), w)))
|
|
lines = append(lines, sepSty.Render(fitWidth(strings.Repeat("─", w), w)))
|
|
|
|
fav := "-"
|
|
if m.history.Favorites[s.ID] {
|
|
fav = "★"
|
|
}
|
|
kvRows := []struct{ k, v string }{
|
|
{"dir", s.Cwd},
|
|
{"time", fmt.Sprintf("%s ~ %s", s.StartTime.Local().Format("01-02 15:04"), s.EndTime.Local().Format("15:04"))},
|
|
{"msgs", fmt.Sprintf("%d", s.MsgCount)},
|
|
{"fav", fav},
|
|
}
|
|
for _, r := range kvRows {
|
|
line := fmt.Sprintf(" %-4s %s", r.k, r.v)
|
|
lines = append(lines, style.NormStyle.Render(fitWidth(line, w)))
|
|
}
|
|
|
|
if summary := s.DisplaySummary(); summary != "" {
|
|
lines = append(lines, sepSty.Render(fitWidth(strings.Repeat("─", w), w)))
|
|
lines = append(lines, style.SessionSummaryStyle.Render(fitWidth(" "+summary, w)))
|
|
}
|
|
|
|
if len(s.Completed) > 0 {
|
|
lines = append(lines, sepSty.Render(fitWidth(strings.Repeat("─", w), w)))
|
|
lines = append(lines, lipgloss.NewStyle().Foreground(style.Success).Bold(true).Render(fitWidth(" ✓ 已完成", w)))
|
|
for _, item := range s.Completed {
|
|
lines = append(lines, style.NormStyle.Render(fitWidth(" · "+cleanText(item), w)))
|
|
}
|
|
}
|
|
|
|
if len(s.Pending) > 0 {
|
|
lines = append(lines, sepSty.Render(fitWidth(strings.Repeat("─", w), w)))
|
|
lines = append(lines, lipgloss.NewStyle().Foreground(style.Warning).Bold(true).Render(fitWidth(" ○ 待办", w)))
|
|
for _, item := range s.Pending {
|
|
lines = append(lines, style.NormStyle.Render(fitWidth(" · "+cleanText(item), w)))
|
|
}
|
|
}
|
|
|
|
lines = append(lines, lipgloss.NewStyle().Foreground(style.Dim).Render(fitWidth(" [Enter]resume", w)))
|
|
|
|
if s.ForkFrom != "" {
|
|
forkID := s.ForkFrom
|
|
if len(forkID) > 8 {
|
|
forkID = forkID[:8]
|
|
}
|
|
lines = append(lines, lipgloss.NewStyle().Foreground(style.Cyan).Render(fitWidth(fmt.Sprintf(" ⎇ fork from %s", forkID), w)))
|
|
}
|
|
|
|
if len(lines) > listH {
|
|
lines = lines[:listH]
|
|
}
|
|
return lines
|
|
}
|