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 }