package tui import ( "fmt" "strings" "time" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/google/uuid" "git.tukangketik.id/swanadiva/hostkeeper/internal/models" ) type keyFormMode int const ( keyFormAdd keyFormMode = iota keyFormEdit ) type keyFieldID int const ( keyFieldName keyFieldID = iota keyFieldType keyFieldPrivateKey keyFieldPassphrase keyFieldCount ) var keyFieldLabels = map[keyFieldID]string{ keyFieldName: "Name", keyFieldType: "Type (rsa/ed25519/ecdsa)", keyFieldPrivateKey: "Private Key (PEM)", keyFieldPassphrase: "Passphrase", } // KeyFormTab is a tab for adding/editing SSH key pairs type KeyFormTab struct { mode keyFormMode editing *models.KeyPair dataDir string inputs []textinput.Model focus keyFieldID width int height int err error saved bool } func NewAddKeyFormTab(dataDir string) *KeyFormTab { return newKeyFormTab(keyFormAdd, nil, dataDir) } func NewEditKeyFormTab(key *models.KeyPair, dataDir string) *KeyFormTab { return newKeyFormTab(keyFormEdit, key, dataDir) } func newKeyFormTab(mode keyFormMode, key *models.KeyPair, dataDir string) *KeyFormTab { inputs := make([]textinput.Model, keyFieldCount) for i := range inputs { inputs[i] = textinput.New() inputs[i].Prompt = "" } inputs[keyFieldName].Placeholder = "My SSH Key" inputs[keyFieldType].Placeholder = "ed25519" inputs[keyFieldType].SetValue("ed25519") inputs[keyFieldPrivateKey].Placeholder = "-----BEGIN OPENSSH PRIVATE KEY-----" inputs[keyFieldPassphrase].Placeholder = "Optional passphrase" if mode == keyFormEdit && key != nil { inputs[keyFieldName].SetValue(key.Name) inputs[keyFieldType].SetValue(key.Type) inputs[keyFieldPrivateKey].SetValue(key.PrivateKey) inputs[keyFieldPassphrase].SetValue(key.Passphrase) } inputs[keyFieldName].Focus() inputs[keyFieldName].Prompt = "> " return &KeyFormTab{ mode: mode, editing: key, dataDir: dataDir, inputs: inputs, } } func (t *KeyFormTab) Name() string { if t.mode == keyFormEdit { return "Edit Key" } return "Add Key" } func (t *KeyFormTab) Init() tea.Cmd { return textinput.Blink } func (t *KeyFormTab) Update(msg tea.Msg) (Tab, tea.Cmd) { if t.saved { return t, nil } switch msg := msg.(type) { case tea.WindowSizeMsg: t.width = msg.Width t.height = msg.Height case tea.KeyMsg: switch msg.String() { case "esc": return t, func() tea.Msg { return closeFormMsg{} } case "enter": if t.focus == keyFieldCount-1 { return t.submit() } t.nextField() case "tab", "down": t.nextField() case "shift+tab", "up": t.prevField() case "ctrl+s": return t.submit() default: var cmd tea.Cmd t.inputs[t.focus], cmd = t.inputs[t.focus].Update(msg) return t, cmd } } return t, nil } func (t *KeyFormTab) nextField() { t.inputs[t.focus].Blur() t.inputs[t.focus].Prompt = "" t.focus++ if t.focus >= keyFieldCount { t.focus = keyFieldCount - 1 } t.inputs[t.focus].Focus() t.inputs[t.focus].Prompt = "> " } func (t *KeyFormTab) prevField() { t.inputs[t.focus].Blur() t.inputs[t.focus].Prompt = "" t.focus-- if t.focus < 0 { t.focus = 0 } t.inputs[t.focus].Focus() t.inputs[t.focus].Prompt = "> " } func (t *KeyFormTab) submit() (Tab, tea.Cmd) { name := t.inputs[keyFieldName].Value() privateKey := t.inputs[keyFieldPrivateKey].Value() if name == "" || privateKey == "" { t.err = fmt.Errorf("name and private key are required") return t, nil } var key *models.KeyPair if t.mode == keyFormEdit && t.editing != nil { key = t.editing key.Name = name key.Type = t.inputs[keyFieldType].Value() key.PrivateKey = privateKey key.Passphrase = t.inputs[keyFieldPassphrase].Value() } else { key = &models.KeyPair{ ID: uuid.New().String(), Name: name, Type: t.inputs[keyFieldType].Value(), PrivateKey: privateKey, Passphrase: t.inputs[keyFieldPassphrase].Value(), CreatedAt: time.Now(), UpdatedAt: time.Now(), } } t.saved = true return t, saveKeyCmd(key, t.dataDir) } func (t *KeyFormTab) View() string { contentW := t.width - 12 if contentW < 50 { contentW = 50 } if contentW > 70 { contentW = 70 } var inner strings.Builder title := "Add SSH Key" if t.mode == keyFormEdit { title = "Edit SSH Key" } inner.WriteString(lipgloss.PlaceHorizontal(contentW, lipgloss.Center, lipgloss.NewStyle().Bold(true).Foreground(gbYellow).Render(title))) inner.WriteString("\n\n") if t.err != nil { inner.WriteString(lipgloss.PlaceHorizontal(contentW, lipgloss.Center, ErrorStyle.Render(fmt.Sprintf("Error: %v", t.err)))) inner.WriteString("\n\n") } for i := keyFieldID(0); i < keyFieldCount; i++ { input := t.inputs[i] label := keyFieldLabels[i] style := SubtitleStyle if i == t.focus { style = HighlightStyle } inner.WriteString(style.Render(label + ":")) inner.WriteString("\n ") inner.WriteString(input.View()) inner.WriteString("\n\n") } inner.WriteString("\n") inner.WriteString(lipgloss.PlaceHorizontal(contentW, lipgloss.Center, SubtitleStyle.Render("Ctrl+Tab:switch Ctrl+Q:close Tab:next Enter:next Ctrl+S:save Esc:cancel"))) box := BorderStyle.Render(inner.String()) var b strings.Builder b.WriteString(lipgloss.PlaceHorizontal(t.width, lipgloss.Center, box)) return b.String() } func (t *KeyFormTab) Close() {}