refactor: move V1 code into v1/ subdirectory
- git mv cmd/ internal/ pkg/ test/ go.mod go.sum Makefile build.sh docs/ v1/ - Create v1/README.md with V1 documentation - Update root README for V1 + V2 structure - V1 still builds (cd v1 && go build ./cmd/hostkeeper) and 105 tests pass - Root is now clean for V2 development
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
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 < 30 {
|
||||
contentW = 30
|
||||
}
|
||||
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")
|
||||
footerText := "Ctrl+Tab:switch Ctrl+Q:close Tab:next Shift+Tab:prev ↑↓:nav Enter:next Ctrl+S:save Esc:cancel"
|
||||
footerWrapped := wrapFooter(footerText, contentW)
|
||||
for _, line := range strings.Split(footerWrapped, "\n") {
|
||||
inner.WriteString(lipgloss.PlaceHorizontal(contentW, lipgloss.Center, SubtitleStyle.Render(line)))
|
||||
inner.WriteString("\n")
|
||||
}
|
||||
|
||||
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() {}
|
||||
Reference in New Issue
Block a user