Initial commit
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user