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 }