diff --git a/internal/tui/components/delete-confirm.go b/internal/tui/components/delete-confirm.go new file mode 100644 index 0000000..21ffa14 --- /dev/null +++ b/internal/tui/components/delete-confirm.go @@ -0,0 +1,197 @@ +package components + +import ( + "fmt" + + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// DeleteConfirmModel represents a deletion confirmation modal with name verification +type DeleteConfirmModel struct { + clientName string + input textinput.Model + Visible bool + Width int + Height int + showErrorMessage bool +} + +// Styles +var ( + deleteModalStyle = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("196")). + Padding(1, 3). + Background(lipgloss.Color("235")) + deleteTitleStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("226")). + Bold(true) + deleteMessageStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("255")). + Width(60) + deleteWarningStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("196")). + Bold(true). + MarginTop(1) + deleteInputStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("255")). + Width(60) + deleteHelpStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("241")). + MarginTop(1) + deleteErrorStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("196")). + Bold(true). + MarginTop(1) + deleteSuccessStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("46")). + Bold(true). + MarginTop(1) +) + +// NewDeleteConfirm creates a new deletion confirmation modal +func NewDeleteConfirm(clientName string, width, height int) *DeleteConfirmModel { + ti := textinput.New() + ti.Placeholder = "Type client name to confirm" + ti.Focus() + ti.CharLimit = 100 + ti.Width = 40 + + return &DeleteConfirmModel{ + clientName: clientName, + input: ti, + Visible: true, + Width: width, + Height: height, + showErrorMessage: false, + } +} + +// Init initializes the deletion confirmation modal +func (m *DeleteConfirmModel) Init() tea.Cmd { + return textinput.Blink +} + +// Update handles messages for the deletion confirmation modal +func (m *DeleteConfirmModel) Update(msg tea.Msg) (*DeleteConfirmModel, tea.Cmd) { + var cmd tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "esc": + m.Visible = false + return m, nil + case "enter": + if m.input.Value() == m.clientName { + m.Visible = false + return m, nil + } + m.showErrorMessage = true + return m, nil + } + } + + m.input, cmd = m.input.Update(msg) + return m, cmd +} + +// View renders the deletion confirmation modal +func (m *DeleteConfirmModel) View() string { + if !m.Visible { + return "" + } + + // Check if input matches client name + matches := m.input.Value() == m.clientName + + // Build warning section + warningText := deleteWarningStyle.Render( + fmt.Sprintf("⚠️ This will permanently delete client '%s'", m.clientName), + ) + + // Build message section + messageText := deleteMessageStyle.Render( + fmt.Sprintf("This action cannot be undone. Please type the client name to confirm deletion."), + ) + + // Build input section + inputSection := lipgloss.JoinVertical( + lipgloss.Left, + "", + deleteInputStyle.Render("Client name:"), + m.input.View(), + ) + + // Build status section + var statusText string + if matches { + statusText = deleteSuccessStyle.Render("✓ Client name matches. Press Enter to confirm deletion.") + } else if m.showErrorMessage { + statusText = deleteErrorStyle.Render("✗ Client name does not match. Please try again.") + } else if m.input.Value() != "" { + statusText = deleteHelpStyle.Render("Client name does not match yet...") + } else { + statusText = deleteHelpStyle.Render("Type the client name to enable confirmation.") + } + + // Build help section + helpText := deleteHelpStyle.Render("Esc to cancel • Enter to confirm (when name matches)") + + // Build modal content + content := lipgloss.JoinVertical( + lipgloss.Left, + deleteTitleStyle.Render("🗑️ Delete Client"), + "", + warningText, + "", + messageText, + inputSection, + statusText, + "", + helpText, + ) + + // Apply modal style + modal := deleteModalStyle.Render(content) + + // Center modal on screen + modalWidth := lipgloss.Width(modal) + modalHeight := lipgloss.Height(modal) + + x := (m.Width - modalWidth) / 2 + if x < 0 { + x = 0 + } + y := (m.Height - modalHeight) / 2 + if y < 0 { + y = 0 + } + + return lipgloss.Place(m.Width, m.Height, + lipgloss.Left, lipgloss.Top, + modal, + lipgloss.WithWhitespaceChars(" "), + lipgloss.WithWhitespaceForeground(lipgloss.Color("235")), + ) +} + +// IsConfirmed returns true if user confirmed the deletion +func (m *DeleteConfirmModel) IsConfirmed() bool { + return !m.Visible && m.input.Value() == m.clientName +} + +// IsCancelled returns true if user cancelled +func (m *DeleteConfirmModel) IsCancelled() bool { + return !m.Visible && m.input.Value() != m.clientName +} + +// Reset resets the modal for reuse +func (m *DeleteConfirmModel) Reset() { + m.input.SetValue("") + m.input.Reset() + m.Visible = true + m.showErrorMessage = false +} diff --git a/internal/tui/screens/detail.go b/internal/tui/screens/detail.go index f0f75b7..2f1b778 100644 --- a/internal/tui/screens/detail.go +++ b/internal/tui/screens/detail.go @@ -17,7 +17,7 @@ type DetailScreen struct { lastHandshake time.Time transferRx string transferTx string - confirmModal *components.ConfirmModel + confirmModal *components.DeleteConfirmModel showConfirm bool clipboardCopied bool clipboardTimer int @@ -97,16 +97,6 @@ func (s *DetailScreen) Update(msg tea.Msg) (Screen, tea.Cmd) { return s, nil } - // Handle Enter key to confirm - switch msg := msg.(type) { - case tea.KeyMsg: - if msg.String() == "enter" && s.confirmModal.IsConfirmed() { - return s, tea.Batch(s.deleteClient(), func() tea.Msg { - return CloseDetailScreenMsg{} - }) - } - } - return s, cmd } @@ -121,13 +111,13 @@ func (s *DetailScreen) Update(msg tea.Msg) (Screen, tea.Cmd) { s.transferTx = msg.transferTx case tea.KeyMsg: switch msg.String() { - case "q", "esc": + case "b", "esc": // Return to list screen - signal parent to switch screens return s, nil case "d": // Show delete confirmation - s.confirmModal = components.NewConfirm( - fmt.Sprintf("Are you sure you want to delete client '%s'?\n\nThis action cannot be undone.", s.client.Name), + s.confirmModal = components.NewDeleteConfirm( + s.client.Name, 80, 24, ) @@ -196,7 +186,7 @@ func (s *DetailScreen) renderContent() string { ) // Add help text - helpText := detailHelpStyle.Render("Actions: [d] Delete • [c] Copy Public Key • [q] Back") + helpText := detailHelpStyle.Render("Actions: [d] Delete • [c] Copy Public Key • [b] Back") content = lipgloss.JoinVertical(lipgloss.Left, content, helpText) // Show clipboard confirmation