11
This commit is contained in:
commit
e6f1776d4f
264 files changed
+54215
No files matched your search
@@ -0,0 +1,199 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"superwork-tui/internal/issue"
|
||||
)
|
||||
|
||||
var columnZhLabel = map[issue.Column]string{
|
||||
issue.ColumnTodo: "待办",
|
||||
issue.ColumnInProgress: "进行中",
|
||||
issue.ColumnReview: "审查",
|
||||
issue.ColumnDone: "完成",
|
||||
}
|
||||
|
||||
func columnDisplay(col issue.Column) string {
|
||||
if zh, ok := columnZhLabel[col]; ok {
|
||||
return fmt.Sprintf("%s(%s)", col, zh)
|
||||
}
|
||||
return string(col)
|
||||
}
|
||||
|
||||
func autoReviewDisplay(v *bool) string {
|
||||
if v == nil {
|
||||
return "(默认)"
|
||||
}
|
||||
if *v {
|
||||
return "✓ True"
|
||||
}
|
||||
return "✗ False"
|
||||
}
|
||||
|
||||
// colorDisplay shows a colored dot plus the short color name (the palette key
|
||||
// with its "terminal.ansi" prefix stripped), matching the VSCode panel.
|
||||
func colorDisplay(key string) string {
|
||||
if key == "" {
|
||||
return "未分配"
|
||||
}
|
||||
dot := "●"
|
||||
if c, ok := keyToColor[key]; ok {
|
||||
dot = lipgloss.NewStyle().Foreground(c).Render("●")
|
||||
}
|
||||
return dot + " " + strings.TrimPrefix(key, "terminal.ansi")
|
||||
}
|
||||
|
||||
func profileDisplay(path string) string {
|
||||
if path == "" {
|
||||
return "(默认)"
|
||||
}
|
||||
base := filepath.Base(path)
|
||||
return strings.TrimSuffix(base, filepath.Ext(base))
|
||||
}
|
||||
|
||||
func prDisplay(iss issue.Issue) string {
|
||||
if iss.PR == "" {
|
||||
return "未关联 PR"
|
||||
}
|
||||
s := "#" + iss.PR
|
||||
if iss.PrMerged {
|
||||
s += "(已合并)"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func orDash(s string) string {
|
||||
if s == "" {
|
||||
return "—"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func orNotLoaded(s string) string {
|
||||
if s == "" {
|
||||
return "未加载"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// detailRows is the ordered field list of the STATE JSON section, mirroring the
|
||||
// VSCode IssueDetailPanel schema (label → formatted value).
|
||||
func detailRows(iss issue.Issue) [][2]string {
|
||||
return [][2]string{
|
||||
{"状态", columnDisplay(iss.Column)},
|
||||
{"头脑风暴会话id", orDash(iss.SessionID)},
|
||||
{"实施会话id", orDash(iss.ImplementSessionID)},
|
||||
{"审查会话id", orDash(iss.ReviewSessionID)},
|
||||
{"测试会话id", orDash(iss.TestSessionID)},
|
||||
{"自动审查", autoReviewDisplay(iss.AutoReview)},
|
||||
{"颜色", colorDisplay(iss.Color)},
|
||||
{"实施配置文件", profileDisplay(iss.ProfilePath)},
|
||||
{"测试配置文件", profileDisplay(iss.TestProfilePath)},
|
||||
{"规格文件", orNotLoaded(iss.SpecFile)},
|
||||
{"计划文件", orNotLoaded(iss.PlanFile)},
|
||||
{"PR 变更摘要", orNotLoaded(iss.PrDiffFile)},
|
||||
{"合并请求", prDisplay(iss)},
|
||||
{"分支", orDash(iss.Branch)},
|
||||
{"工作树", orNotLoaded(iss.WorktreePath)},
|
||||
}
|
||||
}
|
||||
|
||||
// padCells right-pads s with spaces to a target display width.
|
||||
func padCells(s string, w int) string {
|
||||
if gap := w - lipgloss.Width(s); gap > 0 {
|
||||
return s + strings.Repeat(" ", gap)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
var (
|
||||
detailBorderColor = lipgloss.Color("12")
|
||||
detailZebraBG = lipgloss.Color("236")
|
||||
)
|
||||
|
||||
// detailBody builds the two stacked blocks (header + STATE JSON table) shown for
|
||||
// an issue, sized to the given outer width.
|
||||
func detailBody(iss issue.Issue, width int) string {
|
||||
box := lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(detailBorderColor).
|
||||
Width(width)
|
||||
innerW := width - 2 // border-to-border content width
|
||||
|
||||
// cell renders one full-width content line with a 1-space left inset,
|
||||
// optionally with a zebra background, so nothing re-wraps inside the box.
|
||||
cell := func(s string, zebra bool) string {
|
||||
st := lipgloss.NewStyle().Width(innerW)
|
||||
if zebra {
|
||||
st = st.Background(detailZebraBG)
|
||||
}
|
||||
return st.Render(" " + s)
|
||||
}
|
||||
|
||||
hint := lipgloss.NewStyle().Faint(true)
|
||||
|
||||
// ── header block ──
|
||||
titleStyle := lipgloss.NewStyle().Bold(true).Foreground(ColorFor(iss))
|
||||
title := titleStyle.Render(fmt.Sprintf("#%d %s", iss.Number, iss.Title))
|
||||
if iss.Prerequisite > 0 {
|
||||
title += " " + blockedStyle.Render(fmt.Sprintf("等待 #%d 完成", iss.Prerequisite))
|
||||
}
|
||||
header := lipgloss.JoinVertical(lipgloss.Left,
|
||||
cell(title, false),
|
||||
cell("", false),
|
||||
cell(hint.Render("[b] 头脑风暴 [i] 实施 [v] 审查 [t] 测试 [p] PR提交 [g] PR摘要"), false),
|
||||
cell(hint.Render("[f] 配置 [F] 测试配置 [o] 打开文件 [c] 关闭 [d] 删除 esc 返回"), false),
|
||||
)
|
||||
headerBox := box.Render(header)
|
||||
|
||||
// ── STATE JSON table block ──
|
||||
rows := detailRows(iss)
|
||||
labelW := 16
|
||||
sep := " │ "
|
||||
valueW := innerW - 1 - labelW - lipgloss.Width(sep) // 1 = left inset
|
||||
if valueW < 8 {
|
||||
valueW = 8
|
||||
}
|
||||
labelStyle := lipgloss.NewStyle().Faint(true)
|
||||
|
||||
countStr := fmt.Sprintf("%d 项", len(rows))
|
||||
gap := innerW - 1 - lipgloss.Width("STATE JSON") - lipgloss.Width(countStr) - 1
|
||||
if gap < 1 {
|
||||
gap = 1
|
||||
}
|
||||
lines := []string{cell(
|
||||
lipgloss.NewStyle().Bold(true).Render("STATE JSON")+strings.Repeat(" ", gap)+hint.Render(countStr),
|
||||
false,
|
||||
)}
|
||||
for i, r := range rows {
|
||||
line := labelStyle.Render(padCells(r[0], labelW)) + sep + ansi.Truncate(r[1], valueW, "…")
|
||||
lines = append(lines, cell(line, i%2 == 1))
|
||||
}
|
||||
tableBox := box.Render(lipgloss.JoinVertical(lipgloss.Left, lines...))
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, headerBox, tableBox)
|
||||
}
|
||||
|
||||
// detailView renders the issue detail as a centered modal (two stacked blocks)
|
||||
// floating over the board via lipgloss v2 layer compositing.
|
||||
func (m Model) detailView() string {
|
||||
bg := m.boardView()
|
||||
if m.detailIss == nil {
|
||||
return bg
|
||||
}
|
||||
|
||||
width := 84
|
||||
if m.width > 0 && m.width-8 < width {
|
||||
width = m.width - 8
|
||||
}
|
||||
if width < 40 {
|
||||
width = 40
|
||||
}
|
||||
|
||||
modal := detailBody(*m.detailIss, width)
|
||||
return placeOverlayCenter(bg, modal)
|
||||
}
|
||||
Reference in new issue
Block a user