list dns records

This commit is contained in:
juancwu 2026-01-23 16:55:10 +00:00
commit 04ebd05a8f
5 changed files with 244 additions and 3 deletions

View file

@ -48,6 +48,23 @@ func (c *Client) DomainListAll(start int, includeLabels bool) (*DomainListAllRes
return &resp, nil
}
func (c *Client) RetrieveDNSRecords(domain string) (*DNSRecordsResponse, error) {
reqBody := DNSRecordsRequest{
BaseRequest: BaseRequest{
APIKey: c.APIKey,
SecretAPIKey: c.SecretAPIKey,
},
}
var resp DNSRecordsResponse
err := c.post("/dns/retrieve/"+domain, reqBody, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}
func (c *Client) Ping() (*PingResponse, error) {
reqBody := PingRequest{
BaseRequest: BaseRequest{

View file

@ -56,3 +56,25 @@ type DomainLabel struct {
Title string `json:"title"`
Color string `json:"color"`
}
// DNSRecordsRequest is used to get a list of DNS records
type DNSRecordsRequest struct {
BaseRequest
}
// DNSRecord it is what it says it is.
type DNSRecord struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Content string `json:"content"`
TTL any `json:"ttl"`
Priority any `json:"prio"`
Notes string `json:"notes"`
}
// DNSRecordsResponse it is what it says it is.
type DNSRecordsResponse struct {
BaseResponse
Records []DNSRecord `json:"records"`
}

View file

@ -7,6 +7,8 @@ import (
"git.juancwu.dev/juancwu/porkbacon/internal/ui/messages"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/paginator"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
@ -20,6 +22,11 @@ type domainListMsg struct {
Domains []string
}
type dnsRecordListMsg struct {
Status string
Records []string
}
type menuItem struct {
id uint8
title, desc string
@ -39,10 +46,20 @@ const (
utilPing
)
const (
stateNoOp uint8 = iota
stateDNSRetrieveGetDomain
)
type Model struct {
list list.Model
paginator paginator.Model
spinner spinner.Model
textInput textinput.Model
loading bool
state uint8
domains []string
records []string
client *porkbun.Client
err error
output string
@ -67,15 +84,27 @@ func New(client *porkbun.Client) *Model {
p.ActiveDot = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "235", Dark: "252"}).Render("•")
p.InactiveDot = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "250", Dark: "238"}).Render("•")
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
ti := textinput.New()
ti.Placeholder = "example.com"
ti.CharLimit = 156
ti.Width = 20
return &Model{
list: l,
paginator: p,
spinner: s,
textInput: ti,
client: client,
state: 0,
}
}
func (m *Model) Init() tea.Cmd {
return nil
return tea.Sequence(m.spinner.Tick)
}
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@ -86,6 +115,42 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
case tea.KeyMsg:
if m.loading && msg.String() == "ctrl+c" {
return m, tea.Quit
}
if m.state == stateDNSRetrieveGetDomain {
switch msg.String() {
case "enter":
domain := m.textInput.Value()
m.textInput.SetValue("")
m.state = stateNoOp
m.loading = true
cmd := func() tea.Msg {
resp, err := m.client.RetrieveDNSRecords(domain)
if err != nil {
return messages.ErrorMsg(err)
}
var records []string
for _, record := range resp.Records {
records = append(records, renderRecordItem(&record))
}
return dnsRecordListMsg{
Status: resp.Status,
Records: records,
}
}
return m, tea.Batch(cmd, m.spinner.Tick)
case "esc":
m.state = stateNoOp
m.textInput.SetValue("")
return m, nil
}
var cmd tea.Cmd
m.textInput, cmd = m.textInput.Update(msg)
return m, cmd
}
if m.output != "" {
if msg.String() == "esc" {
m.output = ""
@ -104,6 +169,16 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, cmd
}
if len(m.records) > 0 {
var cmd tea.Cmd
m.paginator, cmd = m.paginator.Update(msg)
if msg.String() == "esc" {
m.records = nil
return m, nil
}
return m, cmd
}
if msg.String() == "enter" {
i, ok := m.list.SelectedItem().(menuItem)
if ok {
@ -111,13 +186,24 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
case dnsRecordListMsg:
m.loading = false
m.paginator.PerPage = 1
m.paginator.SetTotalPages(len(msg.Records))
m.paginator.Page = 0
m.domains = msg.Records
return m, nil
case domainListMsg:
m.loading = false
m.paginator.PerPage = 1
m.paginator.SetTotalPages(len(msg.Domains))
m.paginator.Page = 0
m.domains = msg.Domains
return m, nil
case pingResultMsg:
m.loading = false
m.output = fmt.Sprintf("Ping successful!\nYour IP: %s", msg.IP)
return m, nil
@ -127,26 +213,51 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
var cmd tea.Cmd
if m.loading {
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
}
m.list, cmd = m.list.Update(msg)
return m, cmd
}
func (m *Model) View() string {
if m.loading {
return fmt.Sprintf("\n\n %s Loading... press ctl+c to quit\n\n", m.spinner.View())
}
if m.output != "" {
return fmt.Sprintf("%s\n\n(Press Esc to go back)", m.output)
}
if m.state == stateDNSRetrieveGetDomain {
return fmt.Sprintf(
"Enter domain to retrieve records for:\n\n%s\n\n(esc to quit)",
m.textInput.View(),
)
}
if len(m.domains) > 0 {
return fmt.Sprintf("%s\n\n%s\n\n(Press Esc to go back, arrows to navigate)", m.domains[m.paginator.Page], m.paginator.View())
}
if len(m.records) > 0 {
return fmt.Sprintf("%s\n\n%s\n\n(Press Esc to go back, arrows to navigate)", m.records[m.paginator.Page], m.paginator.View())
}
return m.list.View()
}
func (m *Model) handleSelection(i menuItem) (tea.Model, tea.Cmd) {
switch i.id {
case dnsRetrieveRecords:
m.state = stateDNSRetrieveGetDomain
m.textInput.Focus()
return m, textinput.Blink
case domainListAll:
return m, func() tea.Msg {
m.loading = true
cmd := func() tea.Msg {
resp, err := m.client.DomainListAll(0, true)
if err != nil {
return messages.ErrorMsg(err)
@ -161,14 +272,17 @@ func (m *Model) handleSelection(i menuItem) (tea.Model, tea.Cmd) {
Domains: domains,
}
}
return m, tea.Batch(cmd, m.spinner.Tick)
case utilPing:
return m, func() tea.Msg {
m.loading = true
cmd := func() tea.Msg {
resp, err := m.client.Ping()
if err != nil {
return messages.ErrorMsg(err)
}
return pingResultMsg{IP: resp.YourIP}
}
return m, tea.Batch(cmd, m.spinner.Tick)
}
return m, nil
}

View file

@ -0,0 +1,23 @@
package menu
import (
"fmt"
"strings"
"git.juancwu.dev/juancwu/porkbacon/internal/porkbun"
"git.juancwu.dev/juancwu/porkbacon/internal/ui/utils"
)
func renderRecordItem(item *porkbun.DNSRecord) string {
var b strings.Builder
b.WriteString("ID: " + item.ID + "\n")
b.WriteString("Name: " + item.Name + "\n")
b.WriteString("Type: " + item.Type + "\n")
b.WriteString(fmt.Sprintln("TTL:", item.TTL))
b.WriteString(fmt.Sprintln("Priority:", item.Priority))
b.WriteString("Content: ")
b.WriteString(utils.WrapText(item.Content, 80))
b.WriteString("\n")
b.WriteString("Notes: " + item.Notes + "\n")
return b.String()
}

65
internal/ui/utils/text.go Normal file
View file

@ -0,0 +1,65 @@
package utils
import (
"strings"
)
// WrapText wraps text to a limit, forcing mid-word breaks if a word is too long.
// It uses runes to correctly handle UTF-8 multi-byte characters.
func WrapText(text string, limit int) string {
if limit <= 0 {
return text
}
var result strings.Builder
paragraphs := strings.Split(text, "\n")
for i, paragraph := range paragraphs {
words := strings.Fields(paragraph)
currentLineLen := 0
for _, word := range words {
// Convert to runes to handle UTF-8 characters correctly
wordRunes := []rune(word)
wordLen := len(wordRunes)
// Determine if we need a space before the word
if currentLineLen > 0 {
// If adding the word + space exceeds the limit, push to next line
if currentLineLen+1+wordLen <= limit {
result.WriteString(" ")
currentLineLen++
} else {
result.WriteString("\n")
currentLineLen = 0
}
}
// Write the word, chunking it if it exceeds the remaining line limit
for len(wordRunes) > 0 {
spaceLeft := limit - currentLineLen
// If the line is full, wrap to the next line
if spaceLeft <= 0 {
result.WriteString("\n")
currentLineLen = 0
spaceLeft = limit
}
// Take as much of the word as fits on the current line
take := min(len(wordRunes), spaceLeft)
result.WriteString(string(wordRunes[:take]))
currentLineLen += take
wordRunes = wordRunes[take:] // Advance the rune slice
}
}
// Preserve original paragraph breaks
if i < len(paragraphs)-1 {
result.WriteString("\n")
}
}
return result.String()
}