Files
Mester Gábor 2dd6519168 Initial commit
2026-03-19 07:12:03 +01:00

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
}