add action items menu
This commit is contained in:
parent
4b92d2e7c4
commit
4266bfbfc2
5 changed files with 172 additions and 20 deletions
2
go.mod
2
go.mod
|
|
@ -5,7 +5,6 @@ go 1.25.6
|
|||
require (
|
||||
github.com/charmbracelet/bubbles v0.21.0
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/joho/godotenv v1.5.1
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
@ -25,6 +24,7 @@ require (
|
|||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sahilm/fuzzy v0.1.1 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.3.8 // indirect
|
||||
|
|
|
|||
10
go.sum
10
go.sum
|
|
@ -2,6 +2,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
|
|||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||
|
|
@ -14,12 +16,14 @@ github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7
|
|||
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
|
|
@ -37,6 +41,8 @@ github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3
|
|||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
|
||||
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"git.juancwu.dev/juancwu/porkbacon/internal/pass"
|
||||
"git.juancwu.dev/juancwu/porkbacon/internal/porkbun"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
|
@ -13,7 +14,9 @@ import (
|
|||
const (
|
||||
StateInit uint = iota
|
||||
StateLoadingCredentials
|
||||
StateIdle
|
||||
StateMenu
|
||||
StateExecuting
|
||||
StateResult
|
||||
)
|
||||
|
||||
type CredentialsLoadedMsg struct {
|
||||
|
|
@ -21,8 +24,20 @@ type CredentialsLoadedMsg struct {
|
|||
SecretKey string
|
||||
}
|
||||
|
||||
type PingResultMsg struct {
|
||||
IP string
|
||||
}
|
||||
|
||||
type ErrMsg error
|
||||
|
||||
type item struct {
|
||||
title, desc string
|
||||
}
|
||||
|
||||
func (i item) Title() string { return i.title }
|
||||
func (i item) Description() string { return i.desc }
|
||||
func (i item) FilterValue() string { return i.title }
|
||||
|
||||
type App struct {
|
||||
state uint
|
||||
|
||||
|
|
@ -32,8 +47,10 @@ type App struct {
|
|||
|
||||
textInput textinput.Model
|
||||
spinner spinner.Model
|
||||
list list.Model
|
||||
|
||||
err error
|
||||
result string
|
||||
err error
|
||||
}
|
||||
|
||||
func New() App {
|
||||
|
|
@ -45,10 +62,24 @@ func New() App {
|
|||
s := spinner.New()
|
||||
s.Spinner = spinner.Dot
|
||||
|
||||
items := []list.Item{
|
||||
item{title: "Domain: List All", desc: "List all domains in your account"},
|
||||
item{title: "Domain: Get Details", desc: "Get details for a specific domain"},
|
||||
item{title: "DNS: Retrieve Records", desc: "Retrieve DNS records for a domain"},
|
||||
item{title: "DNS: Create Record", desc: "Create a new DNS record"},
|
||||
item{title: "DNS: Edit Record", desc: "Edit an existing DNS record"},
|
||||
item{title: "DNS: Delete Record", desc: "Delete a DNS record"},
|
||||
item{title: "Util: Ping", desc: "Ping Porkbun"},
|
||||
}
|
||||
|
||||
l := list.New(items, list.NewDefaultDelegate(), 0, 0)
|
||||
l.Title = "Porkbun Actions"
|
||||
|
||||
return App{
|
||||
state: StateInit,
|
||||
textInput: ti,
|
||||
spinner: s,
|
||||
list: l,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -65,20 +96,38 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
return a, tea.Quit
|
||||
case "esc":
|
||||
if a.state == StateResult {
|
||||
a.state = StateMenu
|
||||
a.result = ""
|
||||
a.err = nil
|
||||
return a, nil
|
||||
}
|
||||
case "enter":
|
||||
if a.state == StateInit {
|
||||
return a.handleEnter()
|
||||
} else if a.state == StateMenu {
|
||||
return a.handleMenuEnter()
|
||||
}
|
||||
}
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
a.list.SetWidth(msg.Width)
|
||||
a.list.SetHeight(msg.Height)
|
||||
|
||||
case CredentialsLoadedMsg:
|
||||
a.client = porkbun.New(msg.APIKey, msg.SecretKey)
|
||||
a.state = StateIdle
|
||||
a.state = StateMenu
|
||||
return a, nil
|
||||
|
||||
case PingResultMsg:
|
||||
a.result = fmt.Sprintf("Ping successful!\nYour IP: %s", msg.IP)
|
||||
a.state = StateResult
|
||||
return a, nil
|
||||
|
||||
case ErrMsg:
|
||||
a.err = msg
|
||||
a.state = StateIdle // Or stay in error state
|
||||
a.state = StateResult // Show error in result view
|
||||
return a, nil
|
||||
}
|
||||
|
||||
|
|
@ -87,17 +136,26 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
if a.state == StateLoadingCredentials {
|
||||
if a.state == StateLoadingCredentials || a.state == StateExecuting {
|
||||
a.spinner, cmd = a.spinner.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
if a.state == StateMenu {
|
||||
a.list, cmd = a.list.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
return a, tea.Sequence(cmds...)
|
||||
}
|
||||
|
||||
func (a *App) View() string {
|
||||
if a.err != nil {
|
||||
return fmt.Sprintf("Error: %v\n\nPress Ctrl+C to quit.", a.err)
|
||||
if a.state == StateResult {
|
||||
content := a.result
|
||||
if a.err != nil {
|
||||
content = fmt.Sprintf("Error: %v", a.err)
|
||||
}
|
||||
return fmt.Sprintf("%s\n\n(Press Esc to go back)", content)
|
||||
}
|
||||
|
||||
switch a.state {
|
||||
|
|
@ -105,20 +163,14 @@ func (a *App) View() string {
|
|||
return a.renderPorkbunCredentialForm()
|
||||
case StateLoadingCredentials:
|
||||
return fmt.Sprintf("%s Loading credentials...", a.spinner.View())
|
||||
case StateIdle:
|
||||
return fmt.Sprintf("Ready!\nAPI Key: %s...\nSecret: %s...\n",
|
||||
mask(a.client.APIKey), mask(a.client.SecretAPIKey))
|
||||
case StateMenu:
|
||||
return a.list.View()
|
||||
case StateExecuting:
|
||||
return fmt.Sprintf("%s Executing...", a.spinner.View())
|
||||
}
|
||||
return "Invalid state"
|
||||
}
|
||||
|
||||
func mask(s string) string {
|
||||
if len(s) > 4 {
|
||||
return s[:4] + "...."
|
||||
}
|
||||
return "...."
|
||||
}
|
||||
|
||||
func (a *App) renderPorkbunCredentialForm() string {
|
||||
title := "Enter Porkbun API Key Name (pass entry):"
|
||||
if a.apiKeyName != "" {
|
||||
|
|
@ -151,6 +203,30 @@ func (a *App) handleEnter() (tea.Model, tea.Cmd) {
|
|||
return a, nil
|
||||
}
|
||||
|
||||
func (a *App) handleMenuEnter() (tea.Model, tea.Cmd) {
|
||||
i, ok := a.list.SelectedItem().(item)
|
||||
if !ok {
|
||||
return a, nil
|
||||
}
|
||||
|
||||
switch i.title {
|
||||
case "Util: Ping":
|
||||
a.state = StateExecuting
|
||||
return a, a.performPing()
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (a *App) performPing() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
resp, err := a.client.Ping()
|
||||
if err != nil {
|
||||
return ErrMsg(err)
|
||||
}
|
||||
return PingResultMsg{IP: resp.YourIP}
|
||||
}
|
||||
}
|
||||
|
||||
func retrievePorkbunCredentialsCmd(apiKeyName, secretKeyName string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
apiKey, err := pass.Get(apiKeyName)
|
||||
|
|
|
|||
|
|
@ -25,3 +25,49 @@ func New(apiKey, secretKey string) *Client {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Ping() (*PingResponse, error) {
|
||||
reqBody := PingRequest{
|
||||
BaseRequest: BaseRequest{
|
||||
APIKey: c.APIKey,
|
||||
SecretAPIKey: c.SecretAPIKey,
|
||||
},
|
||||
}
|
||||
|
||||
var resp PingResponse
|
||||
err := c.post("/ping", reqBody, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) post(endpoint string, body interface{}, target interface{}) error {
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal request body: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", BaseURL+endpoint, bytes.NewBuffer(jsonBody))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
res, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode >= 400 {
|
||||
return fmt.Errorf("API returned error status: %d", res.StatusCode)
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(res.Body).Decode(target); err != nil {
|
||||
return fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
24
internal/porkbun/models.go
Normal file
24
internal/porkbun/models.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package porkbun
|
||||
|
||||
// BaseRequest contains the authentication fields required for most Porkbun API calls.
|
||||
type BaseRequest struct {
|
||||
APIKey string `json:"apikey"`
|
||||
SecretAPIKey string `json:"secretapikey"`
|
||||
}
|
||||
|
||||
// BaseResponse contains the common fields returned by the Porkbun API.
|
||||
type BaseResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// PingRequest is used to verify credentials.
|
||||
type PingRequest struct {
|
||||
BaseRequest
|
||||
}
|
||||
|
||||
// PingResponse is the response from the ping endpoint.
|
||||
type PingResponse struct {
|
||||
BaseResponse
|
||||
YourIP string `json:"yourIp"`
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue