新增: skills 视图与标题编辑测试(未落库功能批量入库)
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
"u-tabs/internal/style"
|
||||
)
|
||||
|
||||
// --- 技能 tab 三栏渲染 ---
|
||||
|
||||
func (m *Model) renderSkills() string {
|
||||
if !m.Skills.Loaded && !m.Skills.Scanning {
|
||||
return style.ScanningStyle.Render(" press Tab to load skills...")
|
||||
}
|
||||
if m.Skills.Scanning {
|
||||
return style.ScanningStyle.Render(" scanning skills...")
|
||||
}
|
||||
if len(m.Skills.Skills) == 0 {
|
||||
return style.SubtitleStyle.Render(" no skills found in ~/.claude/skills/")
|
||||
}
|
||||
|
||||
avail := m.width - 8
|
||||
skillW := max(18, avail*20/100)
|
||||
fileW := max(22, avail*28/100)
|
||||
contentW := avail - skillW - fileW
|
||||
listH := max(3, m.height-7)
|
||||
|
||||
skillCw := skillW - 2
|
||||
fileCw := fileW - 2
|
||||
contentCw := contentW - 2
|
||||
|
||||
skillLines := m.skillsListLines(skillCw, listH-1)
|
||||
fileLines := m.skillsFileLines(fileCw, listH-1)
|
||||
contentLines := m.skillsContentLines(contentCw, listH-1)
|
||||
|
||||
maxRows := min(listH-1, max(max(len(skillLines), len(fileLines)), len(contentLines)))
|
||||
|
||||
panelSty := lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(style.BgPanel)
|
||||
|
||||
skillBorderFg := style.BgPanel
|
||||
if m.Skills.FocusPanel == 0 {
|
||||
skillBorderFg = style.Accent
|
||||
}
|
||||
fileBorderFg := style.BgPanel
|
||||
if m.Skills.FocusPanel == 1 {
|
||||
fileBorderFg = style.Accent
|
||||
}
|
||||
|
||||
skillBox := panelSty.BorderForeground(skillBorderFg).Width(skillW).Render(
|
||||
style.DetailTitle.Render("skills") + "\n" + padLinesTo(skillLines, skillCw, maxRows))
|
||||
fileBox := panelSty.BorderForeground(fileBorderFg).Width(fileW).Render(
|
||||
style.DetailTitle.Render("files") + "\n" + padLinesTo(fileLines, fileCw, maxRows))
|
||||
contentBox := panelSty.BorderForeground(style.BgPanel).Width(contentW).Render(
|
||||
style.DetailTitle.Render("content") + "\n" + padLinesTo(contentLines, contentCw, maxRows))
|
||||
|
||||
return lipgloss.JoinHorizontal(lipgloss.Top, skillBox, " ", fileBox, " ", contentBox)
|
||||
}
|
||||
|
||||
func (m *Model) skillsListLines(w, listH int) []string {
|
||||
skills := m.Skills.Skills
|
||||
if len(skills) == 0 {
|
||||
return []string{style.DimFg.Render(fitWidth(" empty", w))}
|
||||
}
|
||||
start, end := viewport(m.Skills.SkillCur, len(skills), listH)
|
||||
var lines []string
|
||||
for i := start; i < end; i++ {
|
||||
sk := skills[i]
|
||||
cur := " "
|
||||
if i == m.Skills.SkillCur {
|
||||
cur = "▸"
|
||||
}
|
||||
line := fmt.Sprintf(" %s %s", cur, sk.Name)
|
||||
sty := style.NormStyle
|
||||
if i == m.Skills.SkillCur && m.Skills.FocusPanel == 0 {
|
||||
sty = style.SelStyle
|
||||
}
|
||||
lines = append(lines, sty.Render(padRightByWidth(truncateByWidth(line, w), w)))
|
||||
// 如果有描述,追加一行
|
||||
if sk.Description != "" {
|
||||
descLine := " " + sk.Description
|
||||
descSty := style.DimFg
|
||||
if i == m.Skills.SkillCur && m.Skills.FocusPanel == 0 {
|
||||
descSty = style.SelStyle
|
||||
}
|
||||
lines = append(lines, descSty.Render(padRightByWidth(truncateByWidth(descLine, w), w)))
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func (m *Model) skillsFileLines(w, listH int) []string {
|
||||
flat := m.skillsFlattened()
|
||||
if len(flat) == 0 {
|
||||
return []string{style.DimFg.Render(fitWidth(" ← select skill", w))}
|
||||
}
|
||||
start, end := viewport(m.Skills.FileCur, len(flat), listH)
|
||||
var lines []string
|
||||
for i := start; i < end; i++ {
|
||||
fn := flat[i]
|
||||
indent := strings.Repeat(" ", fn.Depth)
|
||||
cur := " "
|
||||
if i == m.Skills.FileCur {
|
||||
cur = "▸"
|
||||
}
|
||||
prefix := " "
|
||||
if fn.IsDir {
|
||||
if fn.Expanded {
|
||||
prefix = "▾ "
|
||||
} else {
|
||||
prefix = "▸ "
|
||||
}
|
||||
}
|
||||
display := indent + cur + prefix + fn.Name
|
||||
sty := style.NormStyle
|
||||
if fn.IsDir {
|
||||
sty = style.AccentFg
|
||||
}
|
||||
if i == m.Skills.FileCur && m.Skills.FocusPanel == 1 {
|
||||
sty = style.SelStyle
|
||||
}
|
||||
lines = append(lines, sty.Render(padRightByWidth(truncateByWidth(display, w), w)))
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func (m *Model) skillsContentLines(w, listH int) []string {
|
||||
if m.Skills.ContentPath == "" {
|
||||
return []string{style.DimFg.Render(fitWidth(" ← select file to view", w))}
|
||||
}
|
||||
header := style.DetailTitle.Render(fitWidth(" "+m.Skills.ContentPath, w))
|
||||
sep := style.SepFg.Render(fitWidth(sepRunes(w), w))
|
||||
|
||||
var lines []string
|
||||
lines = append(lines, header)
|
||||
lines = append(lines, sep)
|
||||
|
||||
contentLines := strings.Split(m.Skills.Content, "\n")
|
||||
// 内容面板保留标题和分隔线,其余行按滚动偏移显示
|
||||
maxLines := max(0, listH-2)
|
||||
start := m.Skills.ContentScroll
|
||||
if start > len(contentLines) {
|
||||
start = len(contentLines)
|
||||
}
|
||||
end := min(len(contentLines), start+maxLines)
|
||||
for _, line := range contentLines[start:end] {
|
||||
lines = append(lines, style.NormStyle.Render(fitWidth(line, w)))
|
||||
}
|
||||
// 滚动到末尾时自动回收偏移,避免窗口缩小时空白
|
||||
if maxLines > 0 && m.Skills.ContentScroll > max(0, len(contentLines)-maxLines) {
|
||||
m.Skills.ContentScroll = max(0, len(contentLines)-maxLines)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
Reference in New Issue
Block a user