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:
swanadiva
2026-07-07 11:56:27 +07:00
parent 8ebdebedc8
commit 847989df75
68 changed files with 58 additions and 116 deletions
+218
View File
@@ -0,0 +1,218 @@
package tui
import (
"fmt"
"strings"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// passwordPromptMode determines what the password prompt is for
type passwordPromptMode int
const (
// passwordModeSetup — first time enabling encryption, ask for new password
passwordModeSetup passwordPromptMode = iota
// passwordModeUnlock — encryption already enabled, ask for existing password
passwordModeUnlock
)
// PasswordPromptTab shows a password prompt for encryption setup or unlock
type PasswordPromptTab struct {
mode passwordPromptMode
inputs []textinput.Model
focus int
width int
height int
err error
dataDir string
onComplete func(password string) // called with password on success
}
// NewPasswordPromptTab creates a new password prompt tab
func NewPasswordPromptTab(mode passwordPromptMode, dataDir string, onComplete func(string)) *PasswordPromptTab {
p := &PasswordPromptTab{
mode: mode,
dataDir: dataDir,
onComplete: onComplete,
}
// Password field
passwordInput := textinput.New()
passwordInput.Placeholder = "Enter master password"
passwordInput.EchoMode = textinput.EchoPassword
passwordInput.EchoCharacter = '•'
passwordInput.Focus()
// Confirm password field (only for setup mode)
confirmInput := textinput.New()
confirmInput.Placeholder = "Confirm password"
confirmInput.EchoMode = textinput.EchoPassword
confirmInput.EchoCharacter = '•'
if mode == passwordModeSetup {
p.inputs = []textinput.Model{passwordInput, confirmInput}
} else {
p.inputs = []textinput.Model{passwordInput}
}
return p
}
func (p *PasswordPromptTab) Name() string {
if p.mode == passwordModeSetup {
return "Setup Encryption"
}
return "Unlock Storage"
}
func (p *PasswordPromptTab) Init() tea.Cmd {
return textinput.Blink
}
func (p *PasswordPromptTab) Update(msg tea.Msg) (Tab, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
p.width = msg.Width
p.height = msg.Height
return p, nil
case tea.KeyMsg:
switch msg.String() {
case "tab", "down":
p.focus++
if p.focus >= len(p.inputs) {
p.focus = 0
}
for i := range p.inputs {
if i == p.focus {
p.inputs[i].Focus()
} else {
p.inputs[i].Blur()
}
}
return p, nil
case "shift+tab", "up":
p.focus--
if p.focus < 0 {
p.focus = len(p.inputs) - 1
}
for i := range p.inputs {
if i == p.focus {
p.inputs[i].Focus()
} else {
p.inputs[i].Blur()
}
}
return p, nil
case "enter":
password := p.inputs[0].Value()
if password == "" {
p.err = fmt.Errorf("password cannot be empty")
return p, nil
}
if p.mode == passwordModeSetup && len(p.inputs) > 1 {
confirm := p.inputs[1].Value()
if password != confirm {
p.err = fmt.Errorf("passwords do not match")
return p, nil
}
}
// Success — call onComplete
if p.onComplete != nil {
p.onComplete(password)
}
return p, func() tea.Msg { return passwordSetMsg{} }
case "esc":
// Cancel — go back or quit
return p, func() tea.Msg { return closeFormMsg{} }
default:
// Update current input
var cmd tea.Cmd
p.inputs[p.focus], cmd = p.inputs[p.focus].Update(msg)
return p, cmd
}
}
return p, nil
}
func (p *PasswordPromptTab) Close() {
// Nothing to clean up
}
func (p *PasswordPromptTab) View() string {
var b strings.Builder
title := "Setup Master Password"
if p.mode == passwordModeUnlock {
title = "Enter Master Password"
}
b.WriteString(lipgloss.PlaceHorizontal(p.width, lipgloss.Center,
AppTitleStyle.Render(title)))
b.WriteString("\n\n")
if p.mode == passwordModeSetup {
b.WriteString(lipgloss.PlaceHorizontal(p.width, lipgloss.Center,
SubtitleStyle.Render("Encrypt all sensitive data (passwords, keys) with AES-256")))
b.WriteString("\n")
b.WriteString(lipgloss.PlaceHorizontal(p.width, lipgloss.Center,
SubtitleStyle.Render("You will need this password to access your data")))
b.WriteString("\n\n")
}
// Input fields
contentW := p.width - 8
if contentW > 60 {
contentW = 60
}
box := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("62")).
Padding(1, 2).
Width(contentW)
var fields strings.Builder
for i, input := range p.inputs {
label := "Password:"
if i == 1 {
label = "Confirm:"
}
fields.WriteString(SubtitleStyle.Render(" " + label))
fields.WriteString("\n")
fields.WriteString(input.View())
fields.WriteString("\n\n")
}
b.WriteString(lipgloss.PlaceHorizontal(p.width, lipgloss.Center, box.Render(fields.String())))
if p.err != nil {
b.WriteString("\n")
b.WriteString(lipgloss.PlaceHorizontal(p.width, lipgloss.Center,
ErrorStyle.Render(p.err.Error())))
}
// Footer
footerText := "Enter:confirm Esc:cancel Tab:next field"
b.WriteString("\n\n")
for _, line := range strings.Split(wrapFooter(footerText, p.width), "\n") {
b.WriteString(lipgloss.PlaceHorizontal(p.width, lipgloss.Center, SubtitleStyle.Render(line)))
}
return b.String()
}
// passwordSetMsg is sent when password is successfully set
type passwordSetMsg struct {
password string
}