Merge mature Profile editor with contextual Help

This commit is contained in:
proxyctl coordinator
2026-07-18 00:12:54 +08:00
12 changed files with 838 additions and 193 deletions
+40 -102
View File
@@ -21,14 +21,6 @@ const (
formPage
)
type formState struct {
originalName string
profile profile.Profile
field int
probing bool
probeGen uint64
}
type Model struct {
application app.InteractiveModule
presentation terminalPresentation
@@ -211,6 +203,8 @@ func (m Model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
m.form.profile.HTTPURL = httpURL
m.form.profile.HTTPSURL = httpURL
m.form.profile.AllURL = strings.Replace(socksURL, "socks5h://", "socks5://", 1)
m.form.advancedInitialized = true
m.form.rebuildInputs(1)
m.status = "Protocol Probe: detected HTTP and SOCKS5 proxy at " + done.address
return m, nil
}
@@ -220,6 +214,7 @@ func (m Model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
m.form.profile.SingleURL = rewritten
m.form.rebuildInputs(1)
kind := "SOCKS5"
if done.caps.HTTP {
kind = "HTTP"
@@ -500,7 +495,7 @@ func (m Model) updateProfiles(key tea.KeyMsg) (tea.Model, tea.Cmd) {
if !m.profileMutationAvailable("creating a Proxy Profile") {
break
}
m.form = formState{profile: profile.Profile{Mode: profile.SingleMode}}
m.form = newFormState("", profile.Profile{Mode: profile.SingleMode})
m.page = formPage
m.status = ""
case "e", "enter":
@@ -508,7 +503,7 @@ func (m Model) updateProfiles(key tea.KeyMsg) (tea.Model, tea.Cmd) {
break
}
if selected, ok := m.selectedProfile(); ok {
m.form = formState{originalName: selected.Name, profile: selected}
m.form = newFormState(selected.Name, selected)
m.page = formPage
m.status = ""
}
@@ -554,19 +549,39 @@ func newProfileDeleteConfirmation(selected profile.Profile, final, active bool)
}
func (m Model) updateForm(key tea.KeyMsg) (tea.Model, tea.Cmd) {
switch key.String() {
case "q", "esc":
if m.form.currentField() == profile.BypassField {
switch key.String() {
case "pgup":
m.form.scrollBypassPreview(-5)
return m, nil
case "pgdown":
m.form.scrollBypassPreview(5)
return m, nil
}
}
if key.String() == "q" || key.Type == tea.KeyEsc {
m.page = profilesPage
m.status = ""
m.form = formState{}
return m, nil
case "ctrl+s":
}
switch key.Type {
case tea.KeyCtrlS:
m.form.syncFocusedValue()
m.form.refreshValidation()
if err := m.form.validation.Err(); err != nil || m.form.nameConflict(m.snapshot.Profiles) != "" {
m.status = "Fix the highlighted Profile fields before saving"
m.form.probeGen++
m.form.probing = false
return m, nil
}
kind := app.CreateProfile
if m.form.originalName != "" {
kind = app.UpdateProfile
}
intent := app.Intent{Kind: kind, Name: m.form.originalName, Profile: m.form.profile}
result := m.applyProfileMutation(intent, intent.Profile.Name)
normalized := m.form.validation.Normalized
intent := app.Intent{Kind: kind, Name: m.form.originalName, Profile: normalized}
result := m.applyProfileMutation(intent, normalized.Name)
if result.Err != nil {
m.form.probeGen++ // discard any in-flight probe result while staying on the form
m.form.probing = false
@@ -575,12 +590,12 @@ func (m Model) updateForm(key tea.KeyMsg) (tea.Model, tea.Cmd) {
m.page = profilesPage
m.form = formState{}
return m, nil
case "ctrl+d":
case tea.KeyCtrlD:
if m.form.profile.Mode != profile.SingleMode {
m.status = "Protocol Probe: available in Single mode only"
return m, nil
}
if m.form.field != 1 || m.form.probing {
if m.form.currentField() != profile.SingleURLField || m.form.probing {
return m, nil
}
address, err := probeAddress(m.form.profile.SingleURL)
@@ -597,34 +612,15 @@ func (m Model) updateForm(key tea.KeyMsg) (tea.Model, tea.Cmd) {
caps, err := detect(context.Background(), address)
return probeDone{address: address, caps: caps, err: err, gen: gen}
}
case "ctrl+t":
if m.form.profile.Mode == profile.SingleMode {
m.form.profile.Mode = profile.AdvancedMode
m.form.profile.HTTPURL = m.form.profile.SingleURL
m.form.profile.HTTPSURL = m.form.profile.SingleURL
m.form.profile.AllURL = m.form.profile.SingleURL
} else {
m.form.profile.Mode = profile.SingleMode
}
m.form.field = 0
return m, nil
case "tab", "down":
m.form.field = (m.form.field + 1) % m.form.fieldCount()
return m, nil
case "shift+tab", "up":
m.form.field = (m.form.field - 1 + m.form.fieldCount()) % m.form.fieldCount()
return m, nil
case "backspace":
value := []rune(m.form.value())
if len(value) > 0 {
m.form.setValue(string(value[:len(value)-1]))
}
return m, nil
case tea.KeyCtrlT:
return m, m.form.toggleMode()
case tea.KeyTab, tea.KeyDown:
return m, m.form.move(1)
case tea.KeyShiftTab, tea.KeyUp:
return m, m.form.move(-1)
default:
return m, m.form.updateInput(key)
}
if key.Type == tea.KeyRunes {
m.form.setValue(m.form.value() + string(key.Runes))
}
return m, nil
}
func (m Model) beginProfileActivation(intent app.Intent) (tea.Model, tea.Cmd) {
@@ -754,61 +750,3 @@ func (m *Model) retainTargetSelection() {
catalog := newTargetCatalog(m.snapshot, m.showUnavailable, m.selectedTargetID)
m.selectedTargetID = catalog.retain(m.selectedTargetID, catalog.index(m.selectedTargetID))
}
func (f formState) labels() []string {
if f.profile.Mode == profile.AdvancedMode {
return []string{"Name", "HTTP URL", "HTTPS URL", "ALL/SOCKS URL", "Bypass"}
}
return []string{"Name", "Single proxy URL", "Bypass"}
}
func (f formState) fieldCount() int { return len(f.labels()) }
func (f formState) value() string { return f.valueAt(f.field) }
func (f formState) valueAt(index int) string {
if index == 0 {
return f.profile.Name
}
if f.profile.Mode == profile.AdvancedMode {
switch index {
case 1:
return f.profile.HTTPURL
case 2:
return f.profile.HTTPSURL
case 3:
return f.profile.AllURL
default:
return strings.Join(f.profile.Bypass, ", ")
}
}
if index == 1 {
return f.profile.SingleURL
}
return strings.Join(f.profile.Bypass, ", ")
}
func (f *formState) setValue(value string) {
if f.field == 0 {
f.profile.Name = value
return
}
if f.profile.Mode == profile.AdvancedMode {
switch f.field {
case 1:
f.profile.HTTPURL = value
case 2:
f.profile.HTTPSURL = value
case 3:
f.profile.AllURL = value
default:
f.profile.Bypass = []string{value}
}
return
}
if f.field == 1 {
f.profile.SingleURL = value
} else {
f.profile.Bypass = []string{value}
}
}