Files
proxy/internal/tui/pages.go
T

281 lines
10 KiB
Go

package tui
import (
"fmt"
"strings"
"proxyctl/internal/app"
"proxyctl/internal/profile"
)
func (p terminalPresentation) mainView(model presentationState, width, height int) terminalPage {
activeProfile := model.snapshot.ActiveProfile
if activeProfile == "" {
activeProfile = "No Active Profile"
}
header := fmt.Sprintf("%s Operations Console\nActive Profile: %s Targets: %s",
p.theme.title.Render("proxyctl"), activeProfile, p.stateCountSummary(model.snapshot))
if width < wideWidth {
header = fmt.Sprintf("%s Operations Console\nActive Profile: %s\nTargets: %s",
p.theme.title.Render("proxyctl"), activeProfile, p.stateCountSummary(model.snapshot))
}
catalog := newTargetCatalog(model.snapshot, model.showUnavailable, model.selectedTargetID)
footer := p.dashboardFooter(model, catalog)
if model.currentError != nil {
footer += "\ne: expand/collapse current error x: acknowledge error"
if model.errorExpanded {
footer += " PgUp/PgDn: scroll error details"
}
}
page := terminalPage{
header: header,
footer: footer,
dashboard: &dashboardRenderState{
catalog: catalog,
selectedTargetID: model.selectedTargetID,
focus: model.dashboardFocus,
detailsOffset: model.detailsOffset,
operationProgress: model.operationProgress,
},
}
if model.picker.open {
page.dashboard = nil
page.primary = model.picker.view(model.snapshot, width, max(5, height-9))
page.footer = "Type to search ↑/↓ PgUp/PgDn: select Enter: activate\nEsc: cancel ?: help"
if width < 70 {
page.footer = "Type: search ↑/↓: select Enter: activate\nEsc: cancel ?: help"
}
}
if model.driftResolutionTargetID != "" && model.confirmation == nil {
if selected, ok := newTargetCatalog(model.snapshot, true, model.driftResolutionTargetID).selected(model.driftResolutionTargetID); ok {
page.dashboard = nil
page.primary, page.context = p.driftResolutionView(selected)
page.footer = "r: Reapply Active Profile o: Restore Original\nEnter/q/Esc: cancel ?: help"
}
}
if model.confirmation != nil {
dialog := newConfirmationDialog(*model.confirmation)
if model.confirmationDialog != nil {
dialog = *model.confirmationDialog
}
page.dashboard = nil
page.primary = dialog.view()
page.context = "Cancellation is selected by default."
page.footer = "←/→/Tab: choose y: confirm Enter: choose\nn/q/Esc: cancel ?: help"
}
return page
}
func (p terminalPresentation) driftResolutionView(selected app.TargetSnapshot) (string, string) {
var choices strings.Builder
fmt.Fprintf(&choices, "%s\nChoose how to resolve %s without an automatic recovery direction.", p.theme.section.Render("Drift Resolution — "+selected.Description.Name), selected.Description.Name)
for _, kind := range []app.TargetActionKind{app.ReapplyActiveProfileAction, app.RestoreOriginalAction} {
action, ok := driftResolutionAction(selected, kind)
label := targetActionLabel(kind)
if !ok || !action.Available {
reason := action.DisabledReason
if reason == "" {
reason = "unavailable"
}
fmt.Fprintf(&choices, "\n\n%s — unavailable: %s", label, reason)
continue
}
fmt.Fprintf(&choices, "\n\n%s", label)
}
context := "Reapply Active Profile writes the Active Profile again. Restore Original asks the Target Adapter to use its existing ownership and restoration safeguards; insufficient evidence fails safely without overwriting external changes."
return choices.String(), context
}
func (p terminalPresentation) dashboardFooter(model presentationState, catalog targetCatalog) string {
visibility := "u: show unavailable"
if model.showUnavailable {
visibility = "u: hide unavailable"
}
if model.dashboardFocus == targetDetailsPane {
if model.exitDeferred {
return "↑/k ↓/j PgUp/PgDn: browse Tab: focus Targets " + visibility + " ?: help\nExit pending; new work disabled until safe completion"
}
if model.width < wideWidth {
if model.operationActive {
return "↑/k ↓/j PgUp/PgDn: browse Tab: focus Targets\n?: help q/Ctrl+C: defer exit; mutations disabled"
}
if model.width < 70 {
return "↑/k ↓/j PgUp/PgDn: scroll Tab: focus Targets\np: switch Profile m: manage ?: help q: quit"
}
return "↑/k ↓/j PgUp/PgDn: scroll Tab: focus Targets\np: switch Active Profile m: manage Proxy Profiles ?: help q: quit"
}
footer := "↑/k ↓/j PgUp/PgDn: scroll Tab: focus Targets " + visibility + "\np: switch Active Profile m: manage Proxy Profiles ?: help q/Ctrl+C: quit"
if model.operationActive {
footer = "↑/k ↓/j PgUp/PgDn: scroll Tab: focus Targets " + visibility + " ?: help\nq/Ctrl+C: defer safe exit; mutations disabled"
}
return footer
}
actions := "↑/k ↓/j: select Tab: focus details " + visibility
if model.exitDeferred {
return actions + " ?: help\nExit pending; new work disabled until safe completion"
}
selected, ok := catalog.selected(model.selectedTargetID)
availability := targetMutationAvailability(selected, ok, model.operationActive || model.exitDeferred)
label := targetActionLabel(selected.PrimaryAction.Kind)
if availability.available {
actions = "Space: " + label + " " + actions
} else {
actions = "Space: " + label + " disabled (" + availability.compactReason + ") " + actions
}
if model.width < wideWidth {
if availability.available {
actions = "Space: " + label + " ↑/k ↓/j: select Tab: focus details"
} else {
actions = "Space: " + label + " disabled ↑/k ↓/j: select Tab: focus details"
}
if model.operationActive {
return actions + "\n?: help q/Ctrl+C: defer exit; mutations disabled"
}
if model.width < 70 {
return actions + "\np: switch Profile m: manage ?: help q: quit"
}
return actions + "\np: switch Active Profile m: manage Proxy Profiles ?: help q: quit"
}
footer := actions + "\np: switch Active Profile m: manage Proxy Profiles r: refresh ?: help q/Ctrl+C: quit"
if model.operationActive {
footer = actions + "\n?: help q: defer safe exit; new mutations disabled"
}
return footer
}
func (p terminalPresentation) profilesView(model presentationState) terminalPage {
var list strings.Builder
if len(model.snapshot.Profiles) == 0 {
list.WriteString("No Proxy Profiles saved. Press n to create one.")
}
for i, item := range model.snapshot.Profiles {
cursor, active := " ", " "
if i == model.selected {
cursor = "> "
}
if item.Name == model.snapshot.ActiveProfile {
active = "*"
}
if list.Len() > 0 {
list.WriteByte('\n')
}
fmt.Fprintf(&list, "%s%s %s", cursor, active, item.Name)
}
context := selectedProfileDetails(p, model)
footer := "n: create e: edit Enter: edit d: delete PgUp/PgDn: scroll details\n↑/k ↓/j: select ?: help q/Esc: back"
if model.width < 80 {
footer = "↑/k ↓/j: select PgUp/PgDn: details n: create\ne/Enter: edit d: delete ?: help q/Esc: back"
}
if len(model.snapshot.Profiles) == 0 {
footer = "n: create a Proxy Profile ?: help q/Esc: back"
}
if model.profileDelete != nil {
return terminalPage{
header: p.theme.title.Render("Proxy Profile management"),
primary: model.profileDelete.dialog.view(),
footer: "←/→/Tab: choose Enter: choose y: delete\nn/q/Esc: cancel ?: help",
focusLine: -1,
}
}
return terminalPage{
header: p.theme.title.Render("Proxy Profile management"),
primary: list.String(),
context: context,
footer: footer,
focusLine: model.selected,
primaryOffset: model.profileOffset,
contextOffset: model.profileDetails,
}
}
func selectedProfileDetails(p terminalPresentation, model presentationState) string {
selected, ok := profileAt(model.snapshot, model.selected)
if !ok {
return ""
}
var details strings.Builder
details.WriteString(p.theme.section.Render("Selected Profile"))
fmt.Fprintf(&details, "\nMode: %s", selected.Mode)
if selected.Mode == profile.SingleMode {
fmt.Fprintf(&details, "\nProxy: %s", profile.ObscureURL(selected.SingleURL))
} else {
fmt.Fprintf(&details, "\nHTTP: %s\nHTTPS: %s\nALL/SOCKS: %s",
profile.ObscureURL(selected.HTTPURL), profile.ObscureURL(selected.HTTPSURL), profile.ObscureURL(selected.AllURL))
}
details.WriteString("\nBypass:")
if len(selected.Bypass) == 0 {
details.WriteString(" (none)")
} else {
for _, item := range selected.Bypass {
fmt.Fprintf(&details, "\n • %s", item)
}
}
return details.String()
}
func (p terminalPresentation) formView(model presentationState) terminalPage {
action := "Create"
if model.form.originalName != "" {
action = "Edit"
}
var fields, bypassPreview strings.Builder
focusLine, currentLine := 0, 0
for i, item := range model.form.inputs {
if fields.Len() > 0 {
fields.WriteByte('\n')
currentLine++
}
cursor, focus := " ", ""
if i == model.form.field {
cursor, focus = "> ", " [focused]"
focusLine = currentLine
}
fmt.Fprintf(&fields, "%s%s%s: %s", cursor, item.label, focus, model.form.renderedInput(i, model.width))
validation := model.form.validationText(i)
if item.field == profile.NameField {
if conflict := model.form.nameConflict(model.snapshot.Profiles); conflict != "" {
validation = conflict
}
}
if validation != "" {
fmt.Fprintf(&fields, "\n Error: %s", validation)
currentLine++
}
if item.field == profile.BypassField {
analysis := model.form.bypassAnalysis()
bypassPreview.WriteString("Normalized Bypass preview:")
if len(analysis) == 0 {
bypassPreview.WriteString(" (none)")
} else {
for _, line := range analysis {
bypassPreview.WriteString("\n " + line)
}
}
}
}
secondaryHints := "?: help q/Esc: cancel"
if model.form.currentField() == profile.BypassField {
secondaryHints = "PgUp/PgDn: scroll form preview " + secondaryHints
}
if model.form.profile.Mode == profile.SingleMode && model.form.currentField() == profile.SingleURLField {
probeHint := "Ctrl+D: probe"
if model.form.probing {
probeHint = "Protocol Probe in progress"
}
secondaryHints = probeHint + " " + secondaryHints
}
footer := "Tab/Shift+Tab/↑/↓: move Ctrl+T: mode Ctrl+S: save\n" + secondaryHints
return terminalPage{
header: p.theme.title.Render(fmt.Sprintf("%s Proxy Profile — %s mode", action, model.form.profile.Mode)),
primary: fields.String(),
context: bypassPreview.String(),
footer: footer,
focusLine: focusLine,
contextAnchor: model.form.bypassPreviewLine,
anchorContext: true,
}
}