225 lines
5.8 KiB
Go
225 lines
5.8 KiB
Go
package tui
|
|
|
|
import (
|
|
"sort"
|
|
|
|
"greq/internal/api"
|
|
"greq/internal/storage"
|
|
|
|
"github.com/charmbracelet/bubbles/list"
|
|
"github.com/charmbracelet/bubbles/textinput"
|
|
"github.com/charmbracelet/bubbles/viewport"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
// stateCurlImport is shown when the user presses 'i' to import a curl command.
|
|
// After parsing, it transitions to stateWizard with pre-filled inputs.
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// List item types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// folderItem acts as a visual section header; it is not executable.
|
|
type folderItem struct{ name string }
|
|
|
|
func (f folderItem) Title() string { return "" }
|
|
func (f folderItem) Description() string { return "" }
|
|
func (f folderItem) FilterValue() string { return "" }
|
|
|
|
// requestItem wraps a parsed request for display in the list.
|
|
type requestItem struct {
|
|
request storage.RequestFile
|
|
path string
|
|
folder string
|
|
}
|
|
|
|
func (r requestItem) Title() string { return r.request.Name }
|
|
func (r requestItem) Description() string { return r.request.URL }
|
|
func (r requestItem) FilterValue() string {
|
|
return r.request.Name + " " + r.request.URL + " " + r.folder
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Wizard field definitions
|
|
// ---------------------------------------------------------------------------
|
|
|
|
var wizardDefs = []struct{ label, placeholder string }{
|
|
{"Name", "e.g. Login"},
|
|
{"Method", "GET / POST / PUT / DELETE / PATCH"},
|
|
{"URL", "https://api.example.com/endpoint"},
|
|
{"Folder", "(optional) e.g. auth"},
|
|
{"Headers", "(optional) Key:Value, Key2:Value2"},
|
|
{"Body (JSON)", `(optional) {"key": "value"}`},
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// View state
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type viewState int
|
|
|
|
const (
|
|
stateList viewState = iota
|
|
stateWizard // new-request form
|
|
stateCurlImport // single textinput to paste a curl command
|
|
stateEditorPicker // choose default editor when none is configured
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Async messages
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type responseMsg struct {
|
|
req storage.RequestFile
|
|
resp *api.Response
|
|
testResults []api.TestResult
|
|
}
|
|
type errMsg struct {
|
|
req storage.RequestFile
|
|
err error
|
|
}
|
|
type stressResultMsg struct {
|
|
req storage.RequestFile
|
|
result api.StressResult
|
|
}
|
|
type editorDoneMsg struct{}
|
|
type itemsReloadedMsg struct{ items []list.Item }
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Model
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Model is the root Bubble Tea model.
|
|
type Model struct {
|
|
state viewState
|
|
width int
|
|
height int
|
|
|
|
list list.Model
|
|
envs map[string]string
|
|
envList []storage.EnvInfo
|
|
activeEnv int // index into envList
|
|
|
|
vpResp viewport.Model
|
|
vpFocus bool // true → keyboard goes to response viewport
|
|
respText string // current content of the response viewport
|
|
|
|
response *api.Response
|
|
lastRequest *storage.RequestFile
|
|
testResults []api.TestResult
|
|
err error
|
|
loading bool
|
|
statusMsg string
|
|
|
|
// stress test
|
|
stressLoading bool
|
|
stressResult *api.StressResult
|
|
|
|
// history
|
|
history []HistoryEntry
|
|
showHistory bool
|
|
vpHistory viewport.Model
|
|
|
|
// diff overlay
|
|
showDiff bool
|
|
diffText string
|
|
vpDiff viewport.Model
|
|
|
|
// curl import
|
|
curlInput textinput.Model
|
|
|
|
// editor picker
|
|
cfg storage.Config
|
|
pickerEditors []editorChoice // available editors on PATH
|
|
pickerCursor int // currently highlighted row
|
|
pickerTargetPath string // file to open after editor is chosen
|
|
|
|
// wizard
|
|
wizardInputs []textinput.Model
|
|
wizardFocus int
|
|
}
|
|
|
|
// New constructs the initial Model, loading env and requests from disk.
|
|
func New() Model {
|
|
envs, _ := storage.LoadEnv()
|
|
envList, _ := storage.LoadEnvList()
|
|
cfg, _ := storage.LoadConfig()
|
|
items, _ := buildListItems()
|
|
|
|
l := list.New(items, itemDelegate{}, 0, 0)
|
|
l.Title = "Requests"
|
|
l.SetShowHelp(false)
|
|
l.SetShowStatusBar(false)
|
|
l.SetFilteringEnabled(true)
|
|
l.Styles.Title = headerStyle
|
|
|
|
return Model{
|
|
envs: envs,
|
|
envList: envList,
|
|
cfg: cfg,
|
|
list: l,
|
|
}
|
|
}
|
|
|
|
func (m Model) Init() tea.Cmd { return nil }
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// buildListItems loads requests from disk, sorts them by folder then name,
|
|
// and interleaves folderItem headers between groups.
|
|
func buildListItems() ([]list.Item, error) {
|
|
requests, err := storage.LoadRequests()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sort.Slice(requests, func(i, j int) bool {
|
|
a, b := requests[i], requests[j]
|
|
if a.Folder != b.Folder {
|
|
if a.Folder == "" {
|
|
return true
|
|
}
|
|
if b.Folder == "" {
|
|
return false
|
|
}
|
|
return a.Folder < b.Folder
|
|
}
|
|
return a.Request.Name < b.Request.Name
|
|
})
|
|
|
|
var items []list.Item
|
|
lastFolder := "\x00" // sentinel that never equals a real folder
|
|
|
|
for _, r := range requests {
|
|
if r.Folder != lastFolder {
|
|
lastFolder = r.Folder
|
|
if r.Folder != "" {
|
|
items = append(items, folderItem{name: r.Folder})
|
|
}
|
|
}
|
|
items = append(items, requestItem{
|
|
request: r.Request,
|
|
path: r.Path,
|
|
folder: r.Folder,
|
|
})
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
// newWizardInputs returns a fresh slice of textinputs for the "new request" form.
|
|
func newWizardInputs() []textinput.Model {
|
|
inputs := make([]textinput.Model, len(wizardDefs))
|
|
for i, def := range wizardDefs {
|
|
ti := textinput.New()
|
|
ti.Placeholder = def.placeholder
|
|
ti.CharLimit = 512
|
|
if i == 0 {
|
|
ti.Focus()
|
|
}
|
|
inputs[i] = ti
|
|
}
|
|
return inputs
|
|
}
|