Initial commit
This commit is contained in:
@@ -0,0 +1,677 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"greq/internal/api"
|
||||
"greq/internal/storage"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// Window resize is handled regardless of state.
|
||||
if sz, ok := msg.(tea.WindowSizeMsg); ok {
|
||||
m.width, m.height = sz.Width, sz.Height
|
||||
m = m.applySize()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
switch m.state {
|
||||
case stateWizard:
|
||||
return m.updateWizard(msg)
|
||||
case stateCurlImport:
|
||||
return m.updateCurlImport(msg)
|
||||
case stateEditorPicker:
|
||||
return m.updateEditorPicker(msg)
|
||||
default:
|
||||
return m.updateList(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// List / main state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (m Model) updateList(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
|
||||
case tea.KeyMsg:
|
||||
// Let the list handle its own filter-mode keys first.
|
||||
if m.list.FilterState() == list.Filtering {
|
||||
var cmd tea.Cmd
|
||||
m.list, cmd = m.list.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
|
||||
case "n":
|
||||
m.state = stateWizard
|
||||
m.wizardInputs = newWizardInputs()
|
||||
m.wizardFocus = 0
|
||||
m.statusMsg = ""
|
||||
return m, nil
|
||||
|
||||
case "i": // import from cURL
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = "Paste curl command…"
|
||||
ti.CharLimit = 4096
|
||||
ti.Width = m.width - 12
|
||||
ti.Focus()
|
||||
m.curlInput = ti
|
||||
m.state = stateCurlImport
|
||||
return m, nil
|
||||
|
||||
case "c": // copy selected request as cURL
|
||||
if i, ok := m.list.SelectedItem().(requestItem); ok {
|
||||
curl := api.ExportCURL(i.request, m.envs)
|
||||
if err := copyToClipboard(curl); err != nil {
|
||||
// Clipboard tool not available — show the command in the
|
||||
// response panel so the user can manually select & copy it.
|
||||
m.respText = curl
|
||||
m.vpResp.SetContent(curl)
|
||||
m.vpResp.GotoTop()
|
||||
m.statusMsg = clipboardInstallHint()
|
||||
} else {
|
||||
m.statusMsg = "Copied cURL to clipboard ✓"
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "d": // diff current response vs snapshot
|
||||
if m.response != nil && m.lastRequest != nil {
|
||||
name := m.lastRequest.Name
|
||||
if !snapshotExists(name) {
|
||||
m.statusMsg = "No baseline — press S to save current as baseline"
|
||||
} else {
|
||||
old, err := loadSnapshot(name)
|
||||
if err != nil {
|
||||
m.statusMsg = "Snapshot error: " + err.Error()
|
||||
} else {
|
||||
lines := computeDiff(old, m.response.Body)
|
||||
m.diffText = renderDiff(lines, m.vpDiff.Width)
|
||||
m.vpDiff.SetContent(m.diffText)
|
||||
m.vpDiff.GotoTop()
|
||||
m.showDiff = !m.showDiff
|
||||
}
|
||||
}
|
||||
} else {
|
||||
m.statusMsg = "Run a request first"
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "S": // save snapshot baseline (shift+s)
|
||||
if m.response != nil && m.lastRequest != nil {
|
||||
if err := saveSnapshot(m.lastRequest.Name, m.response.Body); err != nil {
|
||||
m.statusMsg = "Snapshot error: " + err.Error()
|
||||
} else {
|
||||
m.statusMsg = "Baseline saved ✓"
|
||||
}
|
||||
} else {
|
||||
m.statusMsg = "Nothing to save"
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "1", "2", "3", "4", "5", "6", "7", "8", "9":
|
||||
idx := int(msg.String()[0]-'0') - 1
|
||||
if idx < len(m.envList) {
|
||||
envs, err := storage.LoadEnvFromFile(m.envList[idx].Path)
|
||||
if err != nil {
|
||||
m.statusMsg = "Env load error: " + err.Error()
|
||||
} else {
|
||||
m.activeEnv = idx
|
||||
m.envs = envs
|
||||
m.statusMsg = "Env: " + m.envList[idx].Name
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "e":
|
||||
if i, ok := m.list.SelectedItem().(requestItem); ok {
|
||||
return m.openInEditor(i.path)
|
||||
}
|
||||
|
||||
case "r":
|
||||
m.statusMsg = "Reloading…"
|
||||
return m, reloadCmd(&m)
|
||||
|
||||
case "h":
|
||||
m.showHistory = !m.showHistory
|
||||
if m.showHistory {
|
||||
content := renderHistoryTable(m.history, m.vpHistory.Width)
|
||||
m.vpHistory.SetContent(content)
|
||||
m.vpHistory.GotoTop()
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "tab":
|
||||
if !m.showHistory {
|
||||
m.vpFocus = !m.vpFocus
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "esc":
|
||||
if m.showDiff {
|
||||
m.showDiff = false
|
||||
return m, nil
|
||||
}
|
||||
if m.showHistory {
|
||||
m.showHistory = false
|
||||
return m, nil
|
||||
}
|
||||
|
||||
case "s": // stress test
|
||||
if i, ok := m.list.SelectedItem().(requestItem); ok {
|
||||
m.stressLoading = true
|
||||
m.stressResult = nil
|
||||
m.response = nil
|
||||
m.lastRequest = nil
|
||||
m.err = nil
|
||||
m.respText = ""
|
||||
m.vpResp.SetContent("")
|
||||
m.statusMsg = fmt.Sprintf("⚡ Running stress test (%d requests)…", api.StressCount)
|
||||
return m, doStressCmd(i.request, m.envs)
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "enter":
|
||||
if i, ok := m.list.SelectedItem().(requestItem); ok {
|
||||
m.loading = true
|
||||
m.statusMsg = "Sending…"
|
||||
m.respText = ""
|
||||
m.response = nil
|
||||
m.lastRequest = nil
|
||||
m.err = nil
|
||||
m.stressResult = nil
|
||||
m.vpResp.SetContent("")
|
||||
return m, doRequestCmd(i.request, m.envs)
|
||||
}
|
||||
// folderItem selected → do nothing
|
||||
return m, nil
|
||||
}
|
||||
|
||||
case responseMsg:
|
||||
m.loading = false
|
||||
m.response = msg.resp
|
||||
m.lastRequest = &msg.req
|
||||
m.testResults = msg.testResults
|
||||
m.err = nil
|
||||
|
||||
highlighted := highlightJSON(msg.resp.Body)
|
||||
m.respText = highlighted
|
||||
m.vpResp.SetContent(highlighted)
|
||||
m.vpResp.GotoTop()
|
||||
|
||||
ct := msg.resp.Headers.Get("Content-Type")
|
||||
if idx := strings.Index(ct, ";"); idx != -1 {
|
||||
ct = strings.TrimSpace(ct[:idx])
|
||||
}
|
||||
m.history = appendHistory(m.history, HistoryEntry{
|
||||
At: timeNow(),
|
||||
Name: msg.req.Name,
|
||||
Method: msg.req.Method,
|
||||
URL: api.ResolveVariables(msg.req.URL, m.envs),
|
||||
StatusCode: msg.resp.StatusCode,
|
||||
Status: msg.resp.Status,
|
||||
Duration: msg.resp.Duration,
|
||||
Size: msg.resp.Size,
|
||||
ContentType: ct,
|
||||
})
|
||||
|
||||
switch {
|
||||
case api.SavedToken != "":
|
||||
m.statusMsg = "Token captured ✓"
|
||||
case testSummary(m.testResults) != "":
|
||||
m.statusMsg = testSummary(m.testResults)
|
||||
default:
|
||||
m.statusMsg = ""
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case errMsg:
|
||||
m.loading = false
|
||||
m.err = msg.err
|
||||
m.lastRequest = &msg.req
|
||||
m.statusMsg = "Error: " + msg.err.Error()
|
||||
errText := "Error:\n\n" + msg.err.Error()
|
||||
m.vpResp.SetContent(errText)
|
||||
m.vpResp.GotoTop()
|
||||
|
||||
m.history = appendHistory(m.history, HistoryEntry{
|
||||
At: timeNow(),
|
||||
Name: msg.req.Name,
|
||||
Method: msg.req.Method,
|
||||
URL: api.ResolveVariables(msg.req.URL, m.envs),
|
||||
IsError: true,
|
||||
ErrMsg: msg.err.Error(),
|
||||
})
|
||||
return m, nil
|
||||
|
||||
case stressResultMsg:
|
||||
m.stressLoading = false
|
||||
m.stressResult = &msg.result
|
||||
m.lastRequest = &msg.req
|
||||
text := renderStressResult(msg.result)
|
||||
m.respText = text
|
||||
m.vpResp.SetContent(text)
|
||||
m.vpResp.GotoTop()
|
||||
m.statusMsg = fmt.Sprintf("⚡ Done: %d requests, %.1f%% errors", msg.result.Total, msg.result.ErrRate())
|
||||
return m, nil
|
||||
|
||||
case editorDoneMsg:
|
||||
m.statusMsg = "Reloading after edit…"
|
||||
return m, reloadCmd(&m)
|
||||
|
||||
case itemsReloadedMsg:
|
||||
m.list.SetItems(msg.items)
|
||||
envs, _ := storage.LoadEnv()
|
||||
m.envs = envs
|
||||
if m.statusMsg == "Reloading…" || m.statusMsg == "Reloading after edit…" {
|
||||
m.statusMsg = ""
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Route keyboard/mouse to the focused panel.
|
||||
var cmd tea.Cmd
|
||||
switch {
|
||||
case m.showDiff:
|
||||
m.vpDiff, cmd = m.vpDiff.Update(msg)
|
||||
case m.showHistory:
|
||||
m.vpHistory, cmd = m.vpHistory.Update(msg)
|
||||
case m.vpFocus:
|
||||
m.vpResp, cmd = m.vpResp.Update(msg)
|
||||
default:
|
||||
m.list, cmd = m.list.Update(msg)
|
||||
}
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Wizard state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (m Model) updateWizard(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "esc", "ctrl+c":
|
||||
m.state = stateList
|
||||
return m, nil
|
||||
|
||||
case "tab", "shift+tab":
|
||||
m.wizardInputs[m.wizardFocus].Blur()
|
||||
if msg.String() == "tab" {
|
||||
m.wizardFocus = (m.wizardFocus + 1) % len(m.wizardInputs)
|
||||
} else {
|
||||
m.wizardFocus = (m.wizardFocus - 1 + len(m.wizardInputs)) % len(m.wizardInputs)
|
||||
}
|
||||
m.wizardInputs[m.wizardFocus].Focus()
|
||||
return m, nil
|
||||
|
||||
case "enter":
|
||||
// On the last field → save and return; otherwise advance.
|
||||
if m.wizardFocus < len(m.wizardInputs)-1 {
|
||||
m.wizardInputs[m.wizardFocus].Blur()
|
||||
m.wizardFocus++
|
||||
m.wizardInputs[m.wizardFocus].Focus()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Save
|
||||
rf, folder := wizardToRequest(m.wizardInputs)
|
||||
if _, err := storage.SaveRequest(rf, folder); err != nil {
|
||||
m.statusMsg = "Save failed: " + err.Error()
|
||||
} else {
|
||||
m.statusMsg = "Saved " + rf.Name
|
||||
}
|
||||
m.state = stateList
|
||||
return m, reloadCmd(&m)
|
||||
}
|
||||
}
|
||||
|
||||
// Forward key events to the focused input.
|
||||
var cmd tea.Cmd
|
||||
m.wizardInputs[m.wizardFocus], cmd = m.wizardInputs[m.wizardFocus].Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Commands
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func doRequestCmd(rf storage.RequestFile, envs map[string]string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
// 1. Run pre-script (shell) to get extra headers
|
||||
extraHeaders, err := api.RunPreScript(rf.PreScript, rf, envs)
|
||||
if err != nil {
|
||||
return errMsg{req: rf, err: err}
|
||||
}
|
||||
// Merge extra headers into a copy of the request
|
||||
if len(extraHeaders) > 0 {
|
||||
merged := make(map[string]string)
|
||||
for k, v := range rf.Headers {
|
||||
merged[k] = v
|
||||
}
|
||||
for k, v := range extraHeaders {
|
||||
merged[k] = v
|
||||
}
|
||||
rf = rf // make a local copy
|
||||
rf.Headers = merged
|
||||
}
|
||||
|
||||
// 2. Execute the HTTP request
|
||||
resp, err := api.Do(rf, envs)
|
||||
if err != nil {
|
||||
return errMsg{req: rf, err: err}
|
||||
}
|
||||
|
||||
// 3. Run test script (Lua)
|
||||
testResults, _ := api.RunTestScript(rf.TestScript, resp)
|
||||
|
||||
return responseMsg{req: rf, resp: resp, testResults: testResults}
|
||||
}
|
||||
}
|
||||
|
||||
func doStressCmd(rf storage.RequestFile, envs map[string]string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
result := api.RunStress(rf, envs)
|
||||
return stressResultMsg{req: rf, result: result}
|
||||
}
|
||||
}
|
||||
|
||||
func reloadCmd(m *Model) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
items, _ := buildListItems()
|
||||
return itemsReloadedMsg{items}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sizing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const listWidth = 36 // total width of the left panel including borders
|
||||
|
||||
// statsBarLines is the number of lines reserved at the bottom of the response
|
||||
// panel for the request-info line + divider + stats bar.
|
||||
const statsBarLines = 3
|
||||
|
||||
func (m Model) applySize() Model {
|
||||
headerH := 1
|
||||
footerH := 2 // divider + key hints
|
||||
bodyH := m.height - headerH - footerH
|
||||
if bodyH < 4 {
|
||||
bodyH = 4
|
||||
}
|
||||
|
||||
leftContent := listWidth - 2 // subtract border chars
|
||||
rightTotal := m.width - listWidth
|
||||
if rightTotal < 4 {
|
||||
rightTotal = 4
|
||||
}
|
||||
rightContent := rightTotal - 2
|
||||
|
||||
m.list.SetSize(leftContent, bodyH-2)
|
||||
|
||||
// Response viewport: leave room for request-info + divider + stats bar
|
||||
vpH := bodyH - 2 - statsBarLines
|
||||
if vpH < 1 {
|
||||
vpH = 1
|
||||
}
|
||||
m.vpResp = viewport.New(rightContent, vpH)
|
||||
m.vpResp.SetContent(m.respText)
|
||||
|
||||
// History viewport
|
||||
ovH := m.height - 8
|
||||
if ovH < 3 {
|
||||
ovH = 3
|
||||
}
|
||||
ovW := m.width - 6
|
||||
if ovW < 20 {
|
||||
ovW = 20
|
||||
}
|
||||
m.vpHistory = viewport.New(ovW-4, ovH)
|
||||
m.vpHistory.SetContent(renderHistoryTable(m.history, ovW-4))
|
||||
|
||||
// Diff viewport (same size as history overlay)
|
||||
m.vpDiff = viewport.New(ovW-4, ovH)
|
||||
m.vpDiff.SetContent(m.diffText)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Wizard helper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// History helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func appendHistory(history []HistoryEntry, e HistoryEntry) []HistoryEntry {
|
||||
history = append(history, e)
|
||||
if len(history) > maxHistory {
|
||||
history = history[len(history)-maxHistory:]
|
||||
}
|
||||
return history
|
||||
}
|
||||
|
||||
// timeNow is a variable so tests can override it.
|
||||
var timeNow = func() time.Time { return time.Now() }
|
||||
|
||||
// testSummary returns a short status string from test results, or "".
|
||||
func testSummary(results []api.TestResult) string {
|
||||
if len(results) == 0 {
|
||||
return ""
|
||||
}
|
||||
var passed, failed int
|
||||
for _, r := range results {
|
||||
if r.Pass {
|
||||
passed++
|
||||
} else {
|
||||
failed++
|
||||
}
|
||||
}
|
||||
if failed == 0 {
|
||||
return fmt.Sprintf("Tests: %d passed ✓", passed)
|
||||
}
|
||||
return fmt.Sprintf("Tests: %d failed, %d passed", failed, passed)
|
||||
}
|
||||
|
||||
// copyToClipboard writes text to the system clipboard.
|
||||
func copyToClipboard(text string) error {
|
||||
return clipboard.WriteAll(text)
|
||||
}
|
||||
|
||||
// clipboardInstallHint returns a short install suggestion based on the detected
|
||||
// desktop session (Wayland vs X11) or falls back to a generic message.
|
||||
func clipboardInstallHint() string {
|
||||
session := strings.ToLower(os.Getenv("XDG_SESSION_TYPE"))
|
||||
switch session {
|
||||
case "wayland":
|
||||
return "Clipboard unavailable — install wl-clipboard: sudo apt install wl-clipboard"
|
||||
case "x11", "x":
|
||||
return "Clipboard unavailable — install xclip: sudo apt install xclip"
|
||||
default:
|
||||
// Could be macOS (pbcopy built-in, shouldn't fail), or unknown.
|
||||
// Show generic hint; the curl is displayed in the panel.
|
||||
return "Clipboard unavailable — curl shown in panel (install xclip / wl-clipboard)"
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Editor resolution + picker
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// openInEditor resolves the editor to use and either launches it immediately
|
||||
// or transitions to stateEditorPicker when no editor is configured/available.
|
||||
func (m Model) openInEditor(path string) (tea.Model, tea.Cmd) {
|
||||
// Priority: 1) .greq/config.yaml 2) $EDITOR 3) picker
|
||||
editorStr := m.cfg.Editor
|
||||
if editorStr == "" {
|
||||
editorStr = os.Getenv("EDITOR")
|
||||
}
|
||||
|
||||
if editorStr != "" {
|
||||
bin, args := editorCommand(editorStr)
|
||||
cmdArgs := append(args, path)
|
||||
c := exec.Command(bin, cmdArgs...)
|
||||
return m, tea.ExecProcess(c, func(err error) tea.Msg {
|
||||
if err != nil {
|
||||
return errMsg{err: err}
|
||||
}
|
||||
return editorDoneMsg{}
|
||||
})
|
||||
}
|
||||
|
||||
// No editor configured — show picker
|
||||
editors := availableEditors()
|
||||
if len(editors) == 0 {
|
||||
m.statusMsg = "No editor found. Set EDITOR or add 'editor:' to .greq/config.yaml"
|
||||
return m, nil
|
||||
}
|
||||
m.state = stateEditorPicker
|
||||
m.pickerEditors = editors
|
||||
m.pickerCursor = 0
|
||||
m.pickerTargetPath = path
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateEditorPicker(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if key, ok := msg.(tea.KeyMsg); ok {
|
||||
switch key.String() {
|
||||
case "esc", "ctrl+c":
|
||||
m.state = stateList
|
||||
return m, nil
|
||||
|
||||
case "up", "k":
|
||||
if m.pickerCursor > 0 {
|
||||
m.pickerCursor--
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "down", "j":
|
||||
if m.pickerCursor < len(m.pickerEditors)-1 {
|
||||
m.pickerCursor++
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "enter", " ":
|
||||
chosen := m.pickerEditors[m.pickerCursor]
|
||||
|
||||
// Ask whether to save as default (second Enter press would be too
|
||||
// complex; we always save — the user can edit config.yaml to change it)
|
||||
m.cfg.Editor = chosen.bin
|
||||
if len(chosen.args) > 0 {
|
||||
m.cfg.Editor = chosen.bin + " " + strings.Join(chosen.args, " ")
|
||||
}
|
||||
_ = storage.SaveConfig(m.cfg)
|
||||
m.statusMsg = "Default editor saved: " + m.cfg.Editor
|
||||
m.state = stateList
|
||||
|
||||
cmdArgs := append(chosen.args, m.pickerTargetPath)
|
||||
c := exec.Command(chosen.bin, cmdArgs...)
|
||||
return m, tea.ExecProcess(c, func(err error) tea.Msg {
|
||||
if err != nil {
|
||||
return errMsg{err: err}
|
||||
}
|
||||
return editorDoneMsg{}
|
||||
})
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// cURL import state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (m Model) updateCurlImport(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "esc", "ctrl+c":
|
||||
m.state = stateList
|
||||
return m, nil
|
||||
case "enter":
|
||||
raw := m.curlInput.Value()
|
||||
rf, err := api.ImportCURL(raw)
|
||||
if err != nil {
|
||||
m.statusMsg = "cURL parse error: " + err.Error()
|
||||
m.state = stateList
|
||||
return m, nil
|
||||
}
|
||||
// Pre-fill wizard inputs with parsed values
|
||||
m.state = stateWizard
|
||||
m.wizardInputs = newWizardInputs()
|
||||
m.wizardInputs[0].SetValue(rf.Name)
|
||||
m.wizardInputs[1].SetValue(rf.Method)
|
||||
m.wizardInputs[2].SetValue(rf.URL)
|
||||
if len(rf.Headers) > 0 {
|
||||
var hparts []string
|
||||
for k, v := range rf.Headers {
|
||||
hparts = append(hparts, k+":"+v)
|
||||
}
|
||||
m.wizardInputs[4].SetValue(strings.Join(hparts, ", "))
|
||||
}
|
||||
m.wizardInputs[5].SetValue(rf.Body)
|
||||
m.wizardFocus = 0
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
m.curlInput, cmd = m.curlInput.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Wizard helper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// wizardToRequest converts the wizard textinputs into a RequestFile and folder.
|
||||
func wizardToRequest(inputs []textinput.Model) (storage.RequestFile, string) {
|
||||
get := func(i int) string {
|
||||
if i < len(inputs) {
|
||||
return strings.TrimSpace(inputs[i].Value())
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
method := strings.ToUpper(get(1))
|
||||
if method == "" {
|
||||
method = "GET"
|
||||
}
|
||||
|
||||
rf := storage.RequestFile{
|
||||
Name: get(0),
|
||||
Method: method,
|
||||
URL: get(2),
|
||||
Body: get(5),
|
||||
}
|
||||
|
||||
// Parse headers: "Key:Value, Key2:Value2"
|
||||
if rawHeaders := get(4); rawHeaders != "" {
|
||||
rf.Headers = make(map[string]string)
|
||||
for _, pair := range strings.Split(rawHeaders, ",") {
|
||||
parts := strings.SplitN(strings.TrimSpace(pair), ":", 2)
|
||||
if len(parts) == 2 {
|
||||
rf.Headers[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
folder := get(3)
|
||||
return rf, folder
|
||||
}
|
||||
Reference in New Issue
Block a user