新增: u-tabs 初始版本
Go TUI 项目启动器,基于 bubbletea v2 + lipgloss v2。 支持分组 Tab、多选启动、编号跳转、Windows Terminal 集成。
This commit is contained in:
387
internal/app.go
Normal file
387
internal/app.go
Normal file
@@ -0,0 +1,387 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
"unicode/utf8"
|
||||
|
||||
"charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"u-tabs/internal/style"
|
||||
)
|
||||
|
||||
// Model 主模型
|
||||
type Model struct {
|
||||
activeGroup int
|
||||
cursor int
|
||||
selected map[int]bool
|
||||
inputBuf string
|
||||
width int
|
||||
height int
|
||||
launched string
|
||||
}
|
||||
|
||||
func NewModel() *Model {
|
||||
return &Model{
|
||||
activeGroup: 0,
|
||||
selected: make(map[int]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyPressMsg:
|
||||
s := msg.String()
|
||||
switch s {
|
||||
case "q", "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "tab", "right", "l":
|
||||
m.activeGroup = (m.activeGroup + 1) % len(Groups)
|
||||
m.cursor = 0
|
||||
m.inputBuf = ""
|
||||
case "shift+tab", "left", "h":
|
||||
m.activeGroup = (m.activeGroup - 1 + len(Groups)) % len(Groups)
|
||||
m.cursor = 0
|
||||
m.inputBuf = ""
|
||||
case "1", "2", "3", "4":
|
||||
idx, _ := strconv.Atoi(s)
|
||||
if idx <= len(Groups) {
|
||||
m.activeGroup = idx - 1
|
||||
m.cursor = 0
|
||||
m.inputBuf = ""
|
||||
}
|
||||
case "up", "k":
|
||||
m.moveCursor(-1)
|
||||
case "down", "j":
|
||||
m.moveCursor(1)
|
||||
case " ":
|
||||
m.toggleMultiSelect()
|
||||
case "enter":
|
||||
if m.inputBuf != "" {
|
||||
return m.launchByInput()
|
||||
}
|
||||
return m.launchSelected()
|
||||
default:
|
||||
if len(s) == 1 && s[0] >= '0' && s[0] <= '9' {
|
||||
m.inputBuf += s
|
||||
}
|
||||
}
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) moveCursor(dir int) {
|
||||
svcs := WorkspacesByGroup(Groups[m.activeGroup].Label)
|
||||
if len(svcs) == 0 {
|
||||
return
|
||||
}
|
||||
m.cursor += dir
|
||||
if m.cursor < 0 {
|
||||
m.cursor = len(svcs) - 1
|
||||
}
|
||||
if m.cursor >= len(svcs) {
|
||||
m.cursor = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) toggleMultiSelect() {
|
||||
svcs := WorkspacesByGroup(Groups[m.activeGroup].Label)
|
||||
if m.cursor < len(svcs) {
|
||||
idx := svcs[m.cursor].Index
|
||||
if m.selected[idx] {
|
||||
delete(m.selected, idx)
|
||||
} else {
|
||||
m.selected[idx] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) launchSelected() (*Model, tea.Cmd) {
|
||||
if len(m.selected) > 0 {
|
||||
var launched []string
|
||||
for idx := range m.selected {
|
||||
ws := &AllWorkspaces[idx]
|
||||
go launchWorkspace(*ws)
|
||||
launched = append(launched, ws.Title)
|
||||
}
|
||||
m.launched = strings.Join(launched, ", ")
|
||||
m.selected = make(map[int]bool)
|
||||
return m, nil
|
||||
}
|
||||
svcs := WorkspacesByGroup(Groups[m.activeGroup].Label)
|
||||
if m.cursor < len(svcs) {
|
||||
ws := svcs[m.cursor]
|
||||
go launchWorkspace(ws)
|
||||
m.launched = ws.Title
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) launchByInput() (*Model, tea.Cmd) {
|
||||
num, err := strconv.Atoi(m.inputBuf)
|
||||
if err != nil {
|
||||
m.inputBuf = ""
|
||||
return m, nil
|
||||
}
|
||||
ws := FindByNumber(num)
|
||||
if ws != nil {
|
||||
go launchWorkspace(*ws)
|
||||
m.launched = ws.Title
|
||||
}
|
||||
m.inputBuf = ""
|
||||
return m, nil
|
||||
}
|
||||
|
||||
|
||||
func (m *Model) View() tea.View {
|
||||
v := tea.NewView(m.render())
|
||||
v.AltScreen = true
|
||||
v.MouseMode = tea.MouseModeCellMotion
|
||||
v.WindowTitle = "u-tabs"
|
||||
return v
|
||||
}
|
||||
|
||||
func (m *Model) render() string {
|
||||
if m.width == 0 {
|
||||
return "loading..."
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
|
||||
// layout widths
|
||||
listW := max(42, min(65, m.width*55/100))
|
||||
detailW := max(30, m.width-listW-3)
|
||||
|
||||
// ── header: title + tabs ──
|
||||
b.WriteString(style.TitleStyle.Render(" u-tabs "))
|
||||
sep := style.TabSep.Render(" | ")
|
||||
for i, g := range Groups {
|
||||
if i > 0 {
|
||||
b.WriteString(sep)
|
||||
}
|
||||
label := fmt.Sprintf(" %s %s ", g.Label, g.Desc)
|
||||
if i == m.activeGroup {
|
||||
gs, ok := style.GroupStyles[g.Label]
|
||||
if ok {
|
||||
b.WriteString(lipgloss.NewStyle().
|
||||
Bold(true).Background(style.BgPanel).Foreground(gs.GetForeground()).
|
||||
Padding(0, 1).Render(label))
|
||||
} else {
|
||||
b.WriteString(style.TabActiveStyle.Render(label))
|
||||
}
|
||||
} else {
|
||||
b.WriteString(style.TabInactiveStyle.Render(label))
|
||||
}
|
||||
}
|
||||
b.WriteString("\n")
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(style.BgPanel).Render(strings.Repeat("─", m.width)))
|
||||
b.WriteString("\n")
|
||||
|
||||
g := Groups[m.activeGroup]
|
||||
gs, _ := style.GroupStyles[g.Label]
|
||||
svcs := WorkspacesByGroup(g.Label)
|
||||
if len(svcs) == 0 {
|
||||
b.WriteString(style.SubtitleStyle.Render(" empty"))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ═══ left: list ═══
|
||||
var left strings.Builder
|
||||
innerW := listW - 4
|
||||
for i, ws := range svcs {
|
||||
cur := " "
|
||||
if i == m.cursor {
|
||||
cur = "▸"
|
||||
}
|
||||
mark := " "
|
||||
if m.selected[ws.Index] {
|
||||
mark = style.MarkStyle.Render("✓")
|
||||
}
|
||||
num := style.NumStyle.Render(fmt.Sprintf("%02d", ws.N))
|
||||
|
||||
prefix := cur + " " + mark + " " + num + " "
|
||||
remainW := max(10, innerW-lipgloss.Width(prefix))
|
||||
text := truncateByWidth(ws.Title+" "+ws.Prompt, remainW)
|
||||
|
||||
line := prefix + text
|
||||
if i == m.cursor {
|
||||
left.WriteString(style.SelStyle.Width(innerW).Render(line))
|
||||
} else {
|
||||
left.WriteString(style.NormStyle.Render(line))
|
||||
}
|
||||
left.WriteString("\n")
|
||||
}
|
||||
|
||||
groupHeader := gs.Render(fmt.Sprintf(" %s · %d ", g.Label, len(svcs)))
|
||||
listBox := lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(style.BgPanel).
|
||||
Width(listW).
|
||||
Padding(0, 1).
|
||||
Render(groupHeader + "\n" + left.String())
|
||||
|
||||
// ═══ right: detail ═══
|
||||
var right strings.Builder
|
||||
if m.cursor < len(svcs) {
|
||||
ws := svcs[m.cursor]
|
||||
|
||||
right.WriteString(style.DetailTitle.Render(" detail "))
|
||||
right.WriteString(lipgloss.NewStyle().Foreground(style.BgPanel).Render(strings.Repeat("─", detailW-10)))
|
||||
right.WriteString("\n\n")
|
||||
|
||||
rows := []struct {
|
||||
key string
|
||||
val string
|
||||
sty lipgloss.Style
|
||||
}{
|
||||
{"dir", ws.Dir, style.ValStyle},
|
||||
{"no", fmt.Sprintf("%02d · %s", ws.N, ws.Group), style.NumStyle},
|
||||
{"desc", ws.Prompt, style.ValStyle},
|
||||
{"tech", ws.Tech, style.TechStyle},
|
||||
{"deploy", ws.Deploy, style.DeployStyle},
|
||||
}
|
||||
for _, r := range rows {
|
||||
right.WriteString(" ")
|
||||
right.WriteString(style.KeyStyle.Render(r.key))
|
||||
right.WriteString(" ")
|
||||
right.WriteString(r.sty.Render(r.val))
|
||||
right.WriteString("\n")
|
||||
}
|
||||
|
||||
right.WriteString("\n")
|
||||
right.WriteString(lipgloss.NewStyle().Foreground(style.BgPanel).Render(" " + strings.Repeat("─", detailW-6)))
|
||||
right.WriteString("\n")
|
||||
right.WriteString(lipgloss.NewStyle().Foreground(style.Accent).Italic(true).Render(" $ wt → CC @" + ws.Title))
|
||||
right.WriteString("\n")
|
||||
right.WriteString(lipgloss.NewStyle().Foreground(style.Dim).Render(" [Enter]start [Space]multi"))
|
||||
} else {
|
||||
right.WriteString(lipgloss.NewStyle().Foreground(style.Dim).Render(" ← select to view"))
|
||||
}
|
||||
|
||||
detailBox := lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(style.BgPanel).
|
||||
Width(detailW).
|
||||
Padding(0, 1).
|
||||
Render(right.String())
|
||||
|
||||
b.WriteString(lipgloss.JoinHorizontal(lipgloss.Left, listBox, " ", detailBox))
|
||||
|
||||
// ── footer ──
|
||||
b.WriteString("\n")
|
||||
if m.inputBuf != "" {
|
||||
b.WriteString(style.InputStyle.Render(fmt.Sprintf(" ▶ num:%s [Enter]go [Esc]cancel", m.inputBuf)))
|
||||
}
|
||||
if m.launched != "" {
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(style.Accent).Render(fmt.Sprintf(" ✓ %s", m.launched)))
|
||||
m.launched = ""
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
helpParts := []string{
|
||||
m.fmtHelp("j/k", "sel"),
|
||||
m.fmtHelp("Enter", "run"),
|
||||
m.fmtHelp("Space", "multi"),
|
||||
m.fmtHelp("Tab", "group"),
|
||||
m.fmtHelp("q", "quit"),
|
||||
}
|
||||
b.WriteString(" " + strings.Join(helpParts, " "))
|
||||
|
||||
if hint := ConfigHint(); hint != "" {
|
||||
b.WriteString("\n" + hint)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m *Model) fmtHelp(key, desc string) string {
|
||||
return lipgloss.NewStyle().Foreground(style.Accent).Render(key) +
|
||||
lipgloss.NewStyle().Foreground(style.Dim).Render(":" + desc)
|
||||
}
|
||||
|
||||
// truncateByWidth 按显示宽度截断字符串,不切断多字节字符
|
||||
func truncateByWidth(s string, maxW int) string {
|
||||
w := 0
|
||||
for i := 0; i < len(s); {
|
||||
_, size := utf8.DecodeRuneInString(s[i:])
|
||||
r := rune(s[i])
|
||||
rw := runeWidth(r)
|
||||
if w+rw > maxW {
|
||||
return s[:i]
|
||||
}
|
||||
w += rw
|
||||
i += size
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// runeWidth 返回单个 rune 的终端显示宽度 (CJK=2, 其他=1)
|
||||
func runeWidth(r rune) int {
|
||||
if r >= 0x1100 &&
|
||||
(r <= 0x115F || r == 0x2329 || r == 0x232A ||
|
||||
(r >= 0x2E80 && r <= 0xA4CF && r != 0x303F) ||
|
||||
(r >= 0xAC00 && r <= 0xD7A3) ||
|
||||
(r >= 0xF900 && r <= 0xFAFF) ||
|
||||
(r >= 0xFE10 && r <= 0xFE19) ||
|
||||
(r >= 0xFE30 && r <= 0xFE6F) ||
|
||||
(r >= 0xFF01 && r <= 0xFF60) ||
|
||||
(r >= 0xFFE0 && r <= 0xFFE6) ||
|
||||
(r >= 0x20000 && r <= 0x2FFFD) ||
|
||||
(r >= 0x30000 && r <= 0x3FFFD)) {
|
||||
return 2
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// --- launch ---
|
||||
|
||||
func launchWorkspace(ws Workspace) {
|
||||
script := buildLaunchScript(ws)
|
||||
encoded := encodePSCommand(script)
|
||||
color := fmt.Sprintf("#%02X%02X%02X", randRange(80, 255), randRange(80, 255), randRange(80, 255))
|
||||
cmd := exec.Command("wt.exe", "-w", "0",
|
||||
"-d", ws.Dir,
|
||||
"--tabColor", color,
|
||||
"pwsh", "-NoExit", "-EncodedCommand", encoded,
|
||||
)
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Printf("[u-tabs] launch fail %s(%d): %v", ws.Title, ws.N, err)
|
||||
}
|
||||
}
|
||||
|
||||
func buildLaunchScript(ws Workspace) string {
|
||||
return fmt.Sprintf(`$Host.UI.RawUI.WindowTitle = "%s"
|
||||
Write-Host "=== %s ===" -ForegroundColor Cyan
|
||||
Write-Host "Prompt: %s" -ForegroundColor Yellow
|
||||
cd "%s"
|
||||
claude --name "%s" --permission-mode bypassPermissions`,
|
||||
ws.Title, ws.Title, ws.Prompt, ws.Dir, ws.Title)
|
||||
}
|
||||
|
||||
func encodePSCommand(script string) string {
|
||||
u16 := utf16.Encode([]rune(script))
|
||||
b := make([]byte, len(u16)*2)
|
||||
for i, r := range u16 {
|
||||
b[i*2] = byte(r)
|
||||
b[i*2+1] = byte(r >> 8)
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
func randRange(min, max int) int {
|
||||
n, _ := rand.Int(rand.Reader, big.NewInt(int64(max-min)))
|
||||
return min + int(n.Int64())
|
||||
}
|
||||
103
internal/config.go
Normal file
103
internal/config.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config YAML 配置结构(嵌套:分组 → 工作空间)
|
||||
type Config struct {
|
||||
Groups []GroupConfig `yaml:"groups"`
|
||||
}
|
||||
|
||||
type GroupConfig struct {
|
||||
Label string `yaml:"label"`
|
||||
Desc string `yaml:"desc"`
|
||||
Base int `yaml:"base"`
|
||||
Items []WorkspaceConfig `yaml:"items"`
|
||||
}
|
||||
|
||||
type WorkspaceConfig struct {
|
||||
Title string `yaml:"title"`
|
||||
Prompt string `yaml:"prompt"`
|
||||
Tech string `yaml:"tech"`
|
||||
Deploy string `yaml:"deploy"`
|
||||
Dir string `yaml:"dir"`
|
||||
}
|
||||
|
||||
// LoadConfig 加载配置文件
|
||||
// 优先级: ~/.u-tabs/config.yaml > exe同目录/config.yaml
|
||||
func LoadConfig() (*Config, error) {
|
||||
// 1. 尝试用户目录
|
||||
userDir, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
userCfg := filepath.Join(userDir, ".u-tabs", "config.yaml")
|
||||
if cfg, err := loadConfigFile(userCfg); err == nil {
|
||||
return cfg, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 尝试 exe 同目录
|
||||
exePath, err := os.Executable()
|
||||
if err == nil {
|
||||
exeDir := filepath.Dir(exePath)
|
||||
exeCfg := filepath.Join(exeDir, "config.yaml")
|
||||
if cfg, err := loadConfigFile(exeCfg); err == nil {
|
||||
return cfg, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("未找到配置文件")
|
||||
}
|
||||
|
||||
func loadConfigFile(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cfg Config
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// ToInternal 将 YAML 配置转换为内部数据结构
|
||||
// 自动生成: index(全局索引), n(编号=base+序号), group(继承父级)
|
||||
func (c *Config) ToInternal() ([]Group, []Workspace) {
|
||||
groups := make([]Group, len(c.Groups))
|
||||
var allWorkspaces []Workspace
|
||||
globalIdx := 0
|
||||
|
||||
for gi, g := range c.Groups {
|
||||
groups[gi] = Group{Label: g.Label, Desc: g.Desc}
|
||||
|
||||
for wi, w := range g.Items {
|
||||
allWorkspaces = append(allWorkspaces, Workspace{
|
||||
Index: globalIdx,
|
||||
N: g.Base + wi,
|
||||
Title: w.Title,
|
||||
Prompt: w.Prompt,
|
||||
Tech: w.Tech,
|
||||
Deploy: w.Deploy,
|
||||
Dir: expandHome(w.Dir),
|
||||
Group: g.Label,
|
||||
})
|
||||
globalIdx++
|
||||
}
|
||||
}
|
||||
return groups, allWorkspaces
|
||||
}
|
||||
|
||||
// expandHome 将 ~ 展开为用户主目录
|
||||
func expandHome(dir string) string {
|
||||
if strings.HasPrefix(dir, "~") {
|
||||
home, _ := os.UserHomeDir()
|
||||
return strings.Replace(dir, "~", home, 1)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
50
internal/style/style.go
Normal file
50
internal/style/style.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package style
|
||||
|
||||
import (
|
||||
"charm.land/lipgloss/v2"
|
||||
)
|
||||
|
||||
// ── palette: cool gray + single accent (teal) ──
|
||||
var (
|
||||
BgDark = lipgloss.Color("#1a1b26") // deep navy
|
||||
BgPanel = lipgloss.Color("#292e42") // panel bg / borders
|
||||
Dim = lipgloss.Color("#565f89") // muted text
|
||||
Fg = lipgloss.Color("#a9b1d6") // normal text
|
||||
Bright = lipgloss.Color("#c0caf5") // bright text
|
||||
Accent = lipgloss.Color("#7aa2f7") // blue accent
|
||||
Success = lipgloss.Color("#9ece6a") // green
|
||||
Warning = lipgloss.Color("#e0af68") // yellow
|
||||
Cyan = lipgloss.Color("#7dcfff") // cyan highlight
|
||||
Purple = lipgloss.Color("#bb9af7") // purple for selected
|
||||
Red = lipgloss.Color("#f7768e") // red
|
||||
|
||||
// ── component styles ──
|
||||
TitleStyle = lipgloss.NewStyle().Bold(true).Foreground(Accent)
|
||||
SubtitleStyle = lipgloss.NewStyle().Foreground(Dim)
|
||||
|
||||
TabActiveStyle = lipgloss.NewStyle().Bold(true).Background(BgPanel).Foreground(Bright).Padding(0, 1)
|
||||
TabInactiveStyle = lipgloss.NewStyle().Foreground(Dim).Padding(0, 1)
|
||||
TabSep = lipgloss.NewStyle().Foreground(BgPanel)
|
||||
|
||||
SelStyle = lipgloss.NewStyle().Foreground(BgDark).Background(Purple).Bold(true)
|
||||
NormStyle = lipgloss.NewStyle().Foreground(Fg)
|
||||
NumStyle = lipgloss.NewStyle().Foreground(Cyan)
|
||||
MarkStyle = lipgloss.NewStyle().Foreground(Success).Bold(true)
|
||||
|
||||
HelpStyle = lipgloss.NewStyle().Foreground(Dim)
|
||||
InputStyle = lipgloss.NewStyle().Foreground(Warning).Bold(true)
|
||||
|
||||
KeyStyle = lipgloss.NewStyle().Foreground(Dim).Width(6).Inline(true)
|
||||
ValStyle = lipgloss.NewStyle().Foreground(Fg).Inline(true)
|
||||
TechStyle = lipgloss.NewStyle().Foreground(Accent).Bold(true).Inline(true)
|
||||
DeployStyle = lipgloss.NewStyle().Foreground(Success).Inline(true)
|
||||
DetailTitle = lipgloss.NewStyle().Foreground(Cyan).Bold(true)
|
||||
)
|
||||
|
||||
var GroupStyles = map[string]lipgloss.Style{
|
||||
"CORE": lipgloss.NewStyle().Bold(true).Foreground(Red),
|
||||
"LAB": lipgloss.NewStyle().Bold(true).Foreground(Success),
|
||||
"TOOLS": lipgloss.NewStyle().Bold(true).Foreground(Warning),
|
||||
"ME": lipgloss.NewStyle().Bold(true).Foreground(Purple),
|
||||
"TEMP": lipgloss.NewStyle().Bold(true).Foreground(Cyan),
|
||||
}
|
||||
80
internal/workspace.go
Normal file
80
internal/workspace.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
"u-tabs/internal/style"
|
||||
)
|
||||
|
||||
// Workspace 工作空间定义
|
||||
type Workspace struct {
|
||||
Index int // 全局索引
|
||||
N int // 编号
|
||||
Title string // 短名
|
||||
Prompt string // 描述
|
||||
Tech string // 技术栈
|
||||
Deploy string // 部署情况
|
||||
Dir string // 目录路径
|
||||
Group string // 分组标签
|
||||
}
|
||||
|
||||
// Group 分组定义
|
||||
type Group struct {
|
||||
Label string
|
||||
Desc string
|
||||
}
|
||||
|
||||
// 运行时数据
|
||||
var Groups []Group
|
||||
var AllWorkspaces []Workspace
|
||||
var wsByNum map[int]*Workspace
|
||||
|
||||
// NoConfig 配置文件不存在标记
|
||||
var NoConfig bool
|
||||
|
||||
func InitData() {
|
||||
cfg, err := LoadConfig()
|
||||
if err != nil {
|
||||
NoConfig = true
|
||||
home, _ := os.UserHomeDir()
|
||||
Groups = []Group{{Label: "HOME", Desc: "默认"}}
|
||||
AllWorkspaces = []Workspace{
|
||||
{Index: 0, N: 0, Title: "home", Prompt: "用户主目录", Tech: "-", Deploy: "本地", Dir: home, Group: "HOME"},
|
||||
}
|
||||
wsByNum = map[int]*Workspace{0: &AllWorkspaces[0]}
|
||||
return
|
||||
}
|
||||
Groups, AllWorkspaces = cfg.ToInternal()
|
||||
wsByNum = make(map[int]*Workspace, len(AllWorkspaces))
|
||||
for i := range AllWorkspaces {
|
||||
wsByNum[AllWorkspaces[i].N] = &AllWorkspaces[i]
|
||||
}
|
||||
}
|
||||
|
||||
func WorkspacesByGroup(groupLabel string) []Workspace {
|
||||
var result []Workspace
|
||||
for _, ws := range AllWorkspaces {
|
||||
if ws.Group == groupLabel {
|
||||
result = append(result, ws)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func FindByNumber(n int) *Workspace {
|
||||
return wsByNum[n]
|
||||
}
|
||||
|
||||
// ConfigHint 配置文件提示文案
|
||||
func ConfigHint() string {
|
||||
if !NoConfig {
|
||||
return ""
|
||||
}
|
||||
home, _ := os.UserHomeDir()
|
||||
path := filepath.Join(home, ".u-tabs", "config.yaml")
|
||||
return lipgloss.NewStyle().Foreground(style.Warning).Bold(true).Render(
|
||||
fmt.Sprintf(" ⚠ 未找到配置文件,请创建: %s (参考 config.example.yaml)", path))
|
||||
}
|
||||
Reference in New Issue
Block a user