243 lines
5.2 KiB
Go
243 lines
5.2 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/bubbles/textinput"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
|
|
"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{
|
|
Name: name,
|
|
Type: t.inputs[keyFieldType].Value(),
|
|
PrivateKey: privateKey,
|
|
Passphrase: t.inputs[keyFieldPassphrase].Value(),
|
|
}
|
|
}
|
|
|
|
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() {}
|