221 lines
8.0 KiB
Go
221 lines
8.0 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.confirmation != nil && !model.operationActive {
|
|
footer = "y: confirm Enter/n/Esc: Cancel q: exit"
|
|
}
|
|
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.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/Esc: Cancel"
|
|
}
|
|
}
|
|
if model.confirmation != nil {
|
|
var details strings.Builder
|
|
details.WriteString(p.theme.section.Render(model.confirmation.Title))
|
|
if model.confirmation.Summary != "" {
|
|
fmt.Fprintf(&details, "\n%s", model.confirmation.Summary)
|
|
}
|
|
for _, detail := range model.confirmation.Details {
|
|
fmt.Fprintf(&details, "\n• %s", detail)
|
|
}
|
|
page.dashboard = nil
|
|
page.primary = details.String()
|
|
page.context = "Cancellation is selected by default."
|
|
}
|
|
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 {
|
|
if model.exitDeferred {
|
|
return "Exit pending; browsing remains safe while the current mutation completes"
|
|
}
|
|
visibility := "u: show unavailable"
|
|
if model.showUnavailable {
|
|
visibility = "u: hide unavailable"
|
|
}
|
|
if model.dashboardFocus == targetDetailsPane {
|
|
footer := "↑/k ↓/j PgUp/PgDn: scroll Tab: focus Targets " + visibility + "\np: manage Proxy Profiles ?: help q: quit"
|
|
if model.operationActive {
|
|
footer = "↑/k ↓/j PgUp/PgDn: scroll Tab: focus Targets ?: help\nMutations disabled: wait for the current mutation to finish"
|
|
}
|
|
return footer
|
|
}
|
|
actions := "↑/k ↓/j: select Tab: focus details " + visibility
|
|
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
|
|
}
|
|
footer := actions + "\np: manage Proxy Profiles r: refresh ?: help q: quit"
|
|
if model.operationActive {
|
|
footer = actions + "\n?: help q: defer safe exit; new mutations disabled"
|
|
}
|
|
return footer
|
|
}
|
|
|
|
func (p terminalPresentation) helpView(model presentationState) terminalPage {
|
|
var body strings.Builder
|
|
for _, item := range model.snapshot.Targets {
|
|
fmt.Fprintf(&body, "%s\n%s\n%s\n", p.theme.section.Render(item.Description.Name), item.Description.Summary, item.Description.Details)
|
|
if item.Available {
|
|
fmt.Fprintf(&body, "Detected: %s\n\n", item.AvailabilityEvidence)
|
|
} else {
|
|
fmt.Fprintf(&body, "Not detected: %s\n\n", item.AvailabilityEvidence)
|
|
}
|
|
}
|
|
return terminalPage{
|
|
header: p.theme.title.Render("proxyctl Help — built-in Targets"),
|
|
primary: strings.TrimRight(body.String(), "\n"),
|
|
footer: "↑/k ↓/j: navigate Space: toggle p: manage Proxy Profiles\nr: refresh ?: help q/Escape: back",
|
|
focusLine: 0,
|
|
}
|
|
}
|
|
|
|
func (p terminalPresentation) profilesView(model presentationState) terminalPage {
|
|
var list strings.Builder
|
|
if len(model.snapshot.Profiles) == 0 {
|
|
list.WriteString("No Proxy Profiles. 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: new e: edit d: delete a/Enter: activate\n↑/k ↓/j: select q/Esc: back"
|
|
switch {
|
|
case model.confirm:
|
|
footer = "y: confirm n/Esc: cancel"
|
|
if len(model.snapshot.Profiles) == 1 {
|
|
context = "Delete Profile — this is the final Proxy Profile. Managed configuration will remain in place and become Drifted."
|
|
} else {
|
|
context = "Delete Profile?"
|
|
}
|
|
}
|
|
return terminalPage{
|
|
header: p.theme.title.Render("Proxy Profiles"),
|
|
primary: list.String(),
|
|
context: context,
|
|
footer: footer,
|
|
focusLine: model.selected,
|
|
}
|
|
}
|
|
|
|
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))
|
|
}
|
|
fmt.Fprintf(&details, "\nBypass: %s", strings.Join(selected.Bypass, ", "))
|
|
return details.String()
|
|
}
|
|
|
|
func (p terminalPresentation) formView(model presentationState) terminalPage {
|
|
action := "Create"
|
|
if model.form.originalName != "" {
|
|
action = "Edit"
|
|
}
|
|
var fields strings.Builder
|
|
for i, label := range model.form.labels() {
|
|
cursor := " "
|
|
if i == model.form.field {
|
|
cursor = "> "
|
|
}
|
|
value := model.form.valueAt(i)
|
|
if label != "Name" && label != "Bypass" {
|
|
value = profile.ObscureURL(value)
|
|
}
|
|
if fields.Len() > 0 {
|
|
fields.WriteByte('\n')
|
|
}
|
|
fmt.Fprintf(&fields, "%s%s: %s", cursor, label, value)
|
|
}
|
|
return terminalPage{
|
|
header: p.theme.title.Render(fmt.Sprintf("%s Proxy Profile — %s mode", action, model.form.profile.Mode)),
|
|
primary: fields.String(),
|
|
footer: "Tab/↑/↓: move Ctrl+T: toggle Single/Advanced\nCtrl+S: save Ctrl+D: detect protocol Esc: cancel",
|
|
focusLine: model.form.field,
|
|
}
|
|
}
|