323 lines
9.2 KiB
Go
323 lines
9.2 KiB
Go
package tui
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"charm.land/bubbles/v2/textinput"
|
|
tea "charm.land/bubbletea/v2"
|
|
|
|
"superwork-tui/internal/auth"
|
|
"superwork-tui/internal/cc"
|
|
"superwork-tui/internal/git"
|
|
"superwork-tui/internal/gitea"
|
|
"superwork-tui/internal/issue"
|
|
"superwork-tui/internal/logging"
|
|
"superwork-tui/internal/store"
|
|
)
|
|
|
|
// ── message types ─────────────────────────────────────────────────────────────
|
|
|
|
type profilePickerLoadedMsg struct {
|
|
profiles []cc.ClaudeProfile
|
|
err error
|
|
}
|
|
|
|
type profilePathSavedMsg struct {
|
|
forTest bool
|
|
err error
|
|
}
|
|
|
|
type profileGridLoadedMsg struct {
|
|
data store.ProfilesData
|
|
err error
|
|
}
|
|
|
|
type profileGridSavedMsg struct {
|
|
err error
|
|
}
|
|
|
|
// ── profile picker (f / F in detail) ─────────────────────────────────────────
|
|
|
|
func loadProfilePickerCmd(dir string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
profiles, err := cc.ListClaudeProfiles(dir)
|
|
return profilePickerLoadedMsg{profiles: profiles, err: err}
|
|
}
|
|
}
|
|
|
|
func (m Model) openProfilePicker(forTest bool) (Model, tea.Cmd) {
|
|
if m.settings == nil || m.settings.ProfilesDir == "" {
|
|
m.statusMsg = "未配置 profiles 目录"
|
|
return m, nil
|
|
}
|
|
m.profilePickerForTest = forTest
|
|
m.profilePickerCursor = 0
|
|
m.profilePickerPrevState = m.state
|
|
m.state = stateProfilePicker
|
|
return m, loadProfilePickerCmd(m.settings.ProfilesDir)
|
|
}
|
|
|
|
func (m Model) handleProfilePickerKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
switch msg.String() {
|
|
case "esc":
|
|
m.state = m.profilePickerPrevState
|
|
case "up", "k":
|
|
if m.profilePickerCursor > 0 {
|
|
m.profilePickerCursor--
|
|
}
|
|
case "down", "j":
|
|
if m.profilePickerCursor < len(m.profilePickerProfiles)-1 {
|
|
m.profilePickerCursor++
|
|
}
|
|
case "enter":
|
|
if len(m.profilePickerProfiles) == 0 {
|
|
m.statusMsg = "无可用 profile"
|
|
m.state = m.profilePickerPrevState
|
|
return m, nil
|
|
}
|
|
chosen := m.profilePickerProfiles[m.profilePickerCursor]
|
|
m.state = m.profilePickerPrevState
|
|
if m.detailIss != nil {
|
|
if m.profilePickerForTest {
|
|
m.detailIss.TestProfilePath = chosen.Path
|
|
logging.Default.Info("tui", fmt.Sprintf("#%d testProfilePath=%s", m.detailIss.Number, chosen.Path))
|
|
return m, saveProfilePathCmd(m.detailIss.Number, chosen.Path, true)
|
|
}
|
|
m.detailIss.ProfilePath = chosen.Path
|
|
logging.Default.Info("tui", fmt.Sprintf("#%d profilePath=%s", m.detailIss.Number, chosen.Path))
|
|
return m, saveProfilePathCmd(m.detailIss.Number, chosen.Path, false)
|
|
}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func saveProfilePathCmd(issueNumber int, path string, forTest bool) tea.Cmd {
|
|
return func() tea.Msg {
|
|
ctx := context.Background()
|
|
root, err := workspaceRootFn()
|
|
if err != nil {
|
|
return profilePathSavedMsg{forTest: forTest, err: err}
|
|
}
|
|
remote, err := git.DetectRepo(root)
|
|
if err != nil {
|
|
return profilePathSavedMsg{forTest: forTest, err: err}
|
|
}
|
|
token, err := auth.ResolveGiteaToken(remote.Host)
|
|
if err != nil {
|
|
return profilePathSavedMsg{forTest: forTest, err: err}
|
|
}
|
|
client := gitea.New(remote.Host, token)
|
|
field := "profilePath"
|
|
if forTest {
|
|
field = "testProfilePath"
|
|
}
|
|
err = issue.MergeStateJSON(ctx, client, remote.Owner, remote.Repo, issueNumber, map[string]any{field: path})
|
|
if err != nil {
|
|
logging.Default.Error("tui", fmt.Sprintf("持久化 %s 失败 #%d: %v", field, issueNumber, err))
|
|
}
|
|
return profilePathSavedMsg{forTest: forTest, err: err}
|
|
}
|
|
}
|
|
|
|
func (m Model) profilePickerView() string {
|
|
var sb strings.Builder
|
|
sb.WriteString(helpStyle.Render("选择 Claude Profile") + "\n")
|
|
sb.WriteString(strings.Repeat("─", 50) + "\n")
|
|
if len(m.profilePickerProfiles) == 0 {
|
|
sb.WriteString(" (无可用 profile — 请检查 profiles 目录)\n")
|
|
} else {
|
|
for i, p := range m.profilePickerProfiles {
|
|
line := fmt.Sprintf(" %-30s %s", p.Name, p.Path)
|
|
if i == m.profilePickerCursor {
|
|
sb.WriteString(selectedCardStyle.Render(line) + "\n")
|
|
} else {
|
|
sb.WriteString(normalCardStyle.Render(line) + "\n")
|
|
}
|
|
}
|
|
}
|
|
sb.WriteString("\n")
|
|
sb.WriteString(helpStyle.Render("↑↓/jk select enter confirm esc cancel"))
|
|
return sb.String()
|
|
}
|
|
|
|
// ── profile grid (G) ──────────────────────────────────────────────────────────
|
|
|
|
func loadProfileGridCmd() tea.Cmd {
|
|
return func() tea.Msg {
|
|
root, err := workspaceRootFn()
|
|
if err != nil {
|
|
return profileGridLoadedMsg{err: err}
|
|
}
|
|
data, err := store.ReadProfiles(root)
|
|
return profileGridLoadedMsg{data: data, err: err}
|
|
}
|
|
}
|
|
|
|
func (m Model) openProfileGrid() (Model, tea.Cmd) {
|
|
m.state = stateProfileGrid
|
|
m.profileGridRow = 0
|
|
m.profileGridCol = 0
|
|
m.profileGridEditing = false
|
|
m.profileGridAddingKey = false
|
|
return m, loadProfileGridCmd()
|
|
}
|
|
|
|
func saveProfileGridCmd(data store.ProfilesData) tea.Cmd {
|
|
return func() tea.Msg {
|
|
root, err := workspaceRootFn()
|
|
if err != nil {
|
|
return profileGridSavedMsg{err: err}
|
|
}
|
|
err = store.WriteProfiles(root, data)
|
|
if err != nil {
|
|
logging.Default.Error("tui", fmt.Sprintf("保存 profiles 失败: %v", err))
|
|
} else {
|
|
logging.Default.Info("tui", "profiles 已保存")
|
|
}
|
|
return profileGridSavedMsg{err: err}
|
|
}
|
|
}
|
|
|
|
func (m Model) handleProfileGridKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
if m.profileGridAddingKey {
|
|
switch msg.String() {
|
|
case "enter":
|
|
key := strings.TrimSpace(m.profileGridNewKey.Value())
|
|
m.profileGridAddingKey = false
|
|
if key != "" {
|
|
vals := make(map[string]string, len(m.profileGridData.Profiles))
|
|
for _, p := range m.profileGridData.Profiles {
|
|
vals[p] = ""
|
|
}
|
|
m.profileGridData.Rows = append(m.profileGridData.Rows, store.ProfileRow{Key: key, Values: vals})
|
|
m.profileGridRow = len(m.profileGridData.Rows) - 1
|
|
m.profileGridCol = 0
|
|
}
|
|
case "esc":
|
|
m.profileGridAddingKey = false
|
|
default:
|
|
var cmd tea.Cmd
|
|
m.profileGridNewKey, cmd = m.profileGridNewKey.Update(msg)
|
|
return m, cmd
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
if m.profileGridEditing {
|
|
switch msg.String() {
|
|
case "enter":
|
|
val := m.profileGridEdit.Value()
|
|
row := m.profileGridRow
|
|
if row < len(m.profileGridData.Rows) && m.profileGridCol >= 1 && m.profileGridCol <= len(m.profileGridData.Profiles) {
|
|
prof := m.profileGridData.Profiles[m.profileGridCol-1]
|
|
if m.profileGridData.Rows[row].Values == nil {
|
|
m.profileGridData.Rows[row].Values = map[string]string{}
|
|
}
|
|
m.profileGridData.Rows[row].Values[prof] = val
|
|
}
|
|
m.profileGridEditing = false
|
|
case "esc":
|
|
m.profileGridEditing = false
|
|
default:
|
|
var cmd tea.Cmd
|
|
m.profileGridEdit, cmd = m.profileGridEdit.Update(msg)
|
|
return m, cmd
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
switch msg.String() {
|
|
case "esc":
|
|
m.state = stateLoaded
|
|
case "up", "k":
|
|
if m.profileGridRow > 0 {
|
|
m.profileGridRow--
|
|
}
|
|
case "down", "j":
|
|
if m.profileGridRow < len(m.profileGridData.Rows)-1 {
|
|
m.profileGridRow++
|
|
}
|
|
case "left", "h":
|
|
if m.profileGridCol > 0 {
|
|
m.profileGridCol--
|
|
}
|
|
case "right", "l":
|
|
if m.profileGridCol < len(m.profileGridData.Profiles) {
|
|
m.profileGridCol++
|
|
}
|
|
case "enter":
|
|
if m.profileGridCol >= 1 && m.profileGridCol <= len(m.profileGridData.Profiles) &&
|
|
m.profileGridRow >= 0 && m.profileGridRow < len(m.profileGridData.Rows) {
|
|
prof := m.profileGridData.Profiles[m.profileGridCol-1]
|
|
curVal := m.profileGridData.Rows[m.profileGridRow].Values[prof]
|
|
ti := textinput.New()
|
|
ti.SetValue(curVal)
|
|
ti.Focus()
|
|
m.profileGridEdit = ti
|
|
m.profileGridEditing = true
|
|
}
|
|
case "a":
|
|
ti := textinput.New()
|
|
ti.Placeholder = "输入行 key"
|
|
ti.Focus()
|
|
m.profileGridNewKey = ti
|
|
m.profileGridAddingKey = true
|
|
case "ctrl+s":
|
|
return m, saveProfileGridCmd(m.profileGridData)
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (m Model) profileGridView() string {
|
|
var sb strings.Builder
|
|
sb.WriteString(helpStyle.Render("Profile Grid") + "\n")
|
|
sb.WriteString(strings.Repeat("─", 60) + "\n")
|
|
|
|
if len(m.profileGridData.Profiles) == 0 {
|
|
sb.WriteString(" (无 profile 列)\n")
|
|
} else {
|
|
header := fmt.Sprintf(" %-20s", "key")
|
|
for _, p := range m.profileGridData.Profiles {
|
|
header += fmt.Sprintf(" %-20s", p)
|
|
}
|
|
sb.WriteString(helpStyle.Render(header) + "\n")
|
|
sb.WriteString(" " + strings.Repeat("─", 20+22*len(m.profileGridData.Profiles)) + "\n")
|
|
|
|
for ri, row := range m.profileGridData.Rows {
|
|
line := ""
|
|
keyCell := fmt.Sprintf("%-20s", row.Key)
|
|
if ri == m.profileGridRow && m.profileGridCol == 0 {
|
|
keyCell = selectedCardStyle.Render(keyCell)
|
|
}
|
|
line += " " + keyCell
|
|
for ci, p := range m.profileGridData.Profiles {
|
|
val := row.Values[p]
|
|
cellStr := fmt.Sprintf("%-20s", val)
|
|
colIdx := ci + 1
|
|
if ri == m.profileGridRow && colIdx == m.profileGridCol {
|
|
if m.profileGridEditing {
|
|
cellStr = m.profileGridEdit.View()
|
|
} else {
|
|
cellStr = selectedCardStyle.Render(cellStr)
|
|
}
|
|
}
|
|
line += " " + cellStr
|
|
}
|
|
sb.WriteString(line + "\n")
|
|
}
|
|
}
|
|
|
|
if m.profileGridAddingKey {
|
|
sb.WriteString("\n新行 key: " + m.profileGridNewKey.View() + "\n")
|
|
}
|
|
|
|
sb.WriteString("\n")
|
|
sb.WriteString(helpStyle.Render("↑↓/jk ←→/hl navigate enter edit cell a add row ctrl+s save esc back"))
|
|
if m.statusMsg != "" {
|
|
sb.WriteString("\n" + helpStyle.Render(m.statusMsg))
|
|
}
|
|
return sb.String()
|
|
}
|