add action items menu
This commit is contained in:
parent
4b92d2e7c4
commit
4266bfbfc2
5 changed files with 172 additions and 20 deletions
|
|
@ -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