| 1 | package tui |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "os" |
| 6 | "strings" |
| 7 | |
| 8 | "github.com/charmbracelet/bubbles/filepicker" |
| 9 | tea "github.com/charmbracelet/bubbletea" |
| 10 | "github.com/charmbracelet/lipgloss" |
| 11 | ) |
| 12 | |
| 13 | func (m Model) showZipFilePicker() (tea.Model, tea.Cmd) { |
| 14 | fp := filepicker.New() |
| 15 | fp.AllowedTypes = []string{".zip"} |
| 16 | fp.ShowHidden = false |
| 17 | fp.ShowSize = true |
| 18 | fp.ShowPermissions = true |
| 19 | fp.DirAllowed = false |
| 20 | fp.FileAllowed = true |
| 21 | fp.AutoHeight = false |
| 22 | fp.SetHeight(20) |
| 23 | fp.Cursor = "▸" |
| 24 | fp.Styles.Cursor = lipgloss.NewStyle().Foreground(lipgloss.Color("170")).Bold(true) |
| 25 | fp.Styles.Selected = lipgloss.NewStyle().Foreground(lipgloss.Color("170")).Bold(true) |
| 26 | fp.Styles.Directory = lipgloss.NewStyle().Foreground(lipgloss.Color("33")).Bold(true) |
| 27 | fp.Styles.Symlink = lipgloss.NewStyle().Foreground(lipgloss.Color("36")) |
| 28 | fp.Styles.File = lipgloss.NewStyle().Foreground(lipgloss.Color("255")) |
| 29 | fp.Styles.DisabledFile = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) |
| 30 | fp.Styles.DisabledCursor = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) |
| 31 | fp.Styles.DisabledSelected = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) |
| 32 | fp.Styles.Permission = lipgloss.NewStyle().Foreground(lipgloss.Color("244")) |
| 33 | fp.Styles.FileSize = lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Width(7).Align(lipgloss.Right) |
| 34 | home, _ := os.UserHomeDir() |
| 35 | if home != "" { |
| 36 | fp.CurrentDirectory = home |
| 37 | } else { |
| 38 | fp.CurrentDirectory, _ = os.Getwd() |
| 39 | } |
| 40 | m.filePicker = fp |
| 41 | m.phase = phaseFilePicker |
| 42 | return m, m.filePicker.Init() |
| 43 | } |
| 44 | |
| 45 | func (m Model) updateFilePicker(msg tea.Msg) (tea.Model, tea.Cmd) { |
| 46 | switch msg := msg.(type) { |
| 47 | case tea.KeyMsg: |
| 48 | if msg.String() == "ctrl+c" { |
| 49 | m.quitting = true |
| 50 | return m, tea.Quit |
| 51 | } |
| 52 | if msg.String() == "q" { |
| 53 | m.phase = phaseSelect |
| 54 | return m, nil |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | var cmd tea.Cmd |
| 59 | m.filePicker, cmd = m.filePicker.Update(msg) |
| 60 | |
| 61 | if didSelect, path := m.filePicker.DidSelectFile(msg); didSelect { |
| 62 | models, err := m.loadVKProxyModelsFromPath(path) |
| 63 | if err != nil { |
| 64 | m.modelsErr = err |
| 65 | m.phase = phaseSelect |
| 66 | return m, nil |
| 67 | } |
| 68 | return m.showVKProxyModels(models) |
| 69 | } |
| 70 | |
| 71 | return m, cmd |
| 72 | } |
| 73 | |
| 74 | func (m Model) viewFilePicker() string { |
| 75 | var b strings.Builder |
| 76 | b.WriteString(logoStyle.Render(logo)) |
| 77 | b.WriteString("\n\n") |
| 78 | b.WriteString(promptStyle.Render(" Select claude-code-config.zip")) |
| 79 | b.WriteString("\n\n") |
| 80 | |
| 81 | // Shorten path: replace home dir with ~. |
| 82 | dir := m.filePicker.CurrentDirectory |
| 83 | if home, _ := os.UserHomeDir(); home != "" { |
| 84 | if dir == home { |
| 85 | dir = "~" |
| 86 | } else if strings.HasPrefix(dir, home+"/") { |
| 87 | dir = "~" + dir[len(home):] |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | panelW := 64 |
| 92 | pathHeader := hintStyle.Render(fmt.Sprintf(" 📁 %s", dir)) |
| 93 | |
| 94 | border := lipgloss.RoundedBorder() |
| 95 | panel := lipgloss.NewStyle(). |
| 96 | Border(border). |
| 97 | BorderForeground(lipgloss.Color("170")). |
| 98 | Padding(0, 1). |
| 99 | Width(panelW). |
| 100 | Render(pathHeader + "\n" + strings.Repeat("─", panelW-4) + "\n" + m.filePicker.View()) |
| 101 | |
| 102 | b.WriteString(panel) |
| 103 | b.WriteString("\n") |
| 104 | b.WriteString(hintStyle.Render(" ↑/↓/pgup/pgdn navigate • enter/→ open • ←/esc back • q cancel")) |
| 105 | |
| 106 | content := b.String() |
| 107 | if m.width > 0 && m.height > 0 { |
| 108 | return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, content) |
| 109 | } |
| 110 | return content |
| 111 | } |
| 112 | |