fix: finish profile editor preview scrolling
This commit is contained in:
@@ -263,12 +263,12 @@ func normalizeBypassEntry(raw string) (string, error) {
|
||||
return host, nil
|
||||
}
|
||||
|
||||
var credentialPattern = regexp.MustCompile(`(?i)(https?|socks5h?)://([^/@\s:]+):([^@\s]*)@`)
|
||||
var credentialPattern = regexp.MustCompile(`(?i)((?:https?|socks5h?)://)?([^/@\s:]+):([^@\s]*)@`)
|
||||
|
||||
// RedactText removes URL passwords from diagnostic text without requiring the
|
||||
// surrounding text to be a valid URL.
|
||||
// surrounding text to be a valid URL or to include a scheme.
|
||||
func RedactText(value string) string {
|
||||
return credentialPattern.ReplaceAllString(value, `${1}://${2}:REDACTED@`)
|
||||
return credentialPattern.ReplaceAllString(value, `${1}${2}:REDACTED@`)
|
||||
}
|
||||
|
||||
func ObscureURL(value string) string {
|
||||
|
||||
@@ -540,6 +540,16 @@ func newProfileDeleteConfirmation(selected profile.Profile, final, active bool)
|
||||
}
|
||||
|
||||
func (m Model) updateForm(key tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
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
|
||||
}
|
||||
}
|
||||
switch key.Type {
|
||||
case tea.KeyEsc:
|
||||
m.page = profilesPage
|
||||
|
||||
+11
-10
@@ -208,7 +208,7 @@ func (p terminalPresentation) formView(model presentationState) terminalPage {
|
||||
if model.form.originalName != "" {
|
||||
action = "Edit"
|
||||
}
|
||||
var fields strings.Builder
|
||||
var fields, bypassPreview strings.Builder
|
||||
focusLine, currentLine := 0, 0
|
||||
for i, item := range model.form.inputs {
|
||||
if fields.Len() > 0 {
|
||||
@@ -233,22 +233,23 @@ func (p terminalPresentation) formView(model presentationState) terminalPage {
|
||||
}
|
||||
if item.field == profile.BypassField {
|
||||
analysis := model.form.bypassAnalysis()
|
||||
fields.WriteString("\n Normalized Bypass preview:")
|
||||
currentLine++
|
||||
bypassPreview.WriteString("Normalized Bypass preview:")
|
||||
if len(analysis) == 0 {
|
||||
fields.WriteString(" (none)")
|
||||
bypassPreview.WriteString(" (none)")
|
||||
} else {
|
||||
for _, line := range analysis {
|
||||
fields.WriteString("\n " + line)
|
||||
currentLine++
|
||||
bypassPreview.WriteString("\n " + line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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: focusLine,
|
||||
header: p.theme.title.Render(fmt.Sprintf("%s Proxy Profile — %s mode", action, model.form.profile.Mode)),
|
||||
primary: fields.String(),
|
||||
context: bypassPreview.String(),
|
||||
footer: "Tab/↑/↓: move Ctrl+T: toggle Single/Advanced PgUp/PgDn: scroll form\nCtrl+S: save Ctrl+D: detect protocol Esc: cancel",
|
||||
focusLine: focusLine,
|
||||
contextAnchor: model.form.bypassPreviewLine,
|
||||
anchorContext: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,8 @@ type terminalPage struct {
|
||||
focusLine int
|
||||
primaryOffset int
|
||||
contextOffset int
|
||||
contextAnchor int
|
||||
anchorContext bool
|
||||
dashboard *dashboardRenderState
|
||||
}
|
||||
|
||||
@@ -225,7 +227,11 @@ func renderPageBody(page terminalPage, width, height int) string {
|
||||
contextHeight = max(1, height-primaryHeight-1)
|
||||
}
|
||||
primary := viewportContentAt(page.primary, width, primaryHeight, page.primaryOffset, page.focusLine)
|
||||
context := viewportContentAt(page.context, width, contextHeight, page.contextOffset, -1)
|
||||
contextFocus := -1
|
||||
if page.anchorContext {
|
||||
contextFocus = page.contextAnchor
|
||||
}
|
||||
context := viewportContentAt(page.context, width, contextHeight, page.contextOffset, contextFocus)
|
||||
return primary + "\n\n" + context
|
||||
}
|
||||
|
||||
|
||||
@@ -167,7 +167,70 @@ func TestCompleteModelProfileEditorScrollsLongBypassPreviewAt80Columns(t *testin
|
||||
model = updateKey(model, "pgdown")
|
||||
}
|
||||
model = resize(model, 80, 24)
|
||||
assertUsableFrame(t, model.View(), 80, 24, "! item 25", "late invalid entry", "invalid", "PgUp/PgDn: scroll form")
|
||||
assertUsableFrame(t, model.View(), 80, 24,
|
||||
"> Bypass [focused]", "! item 25", "late invalid entry", "invalid", "PgUp/PgDn: scroll form")
|
||||
|
||||
model = updateKey(model, "pgup")
|
||||
view := model.View()
|
||||
assertUsableFrame(t, view, 80, 24, "> Bypass [focused]", "host-19.example", "PgUp/PgDn: scroll form")
|
||||
if strings.Contains(view, "! item 25") {
|
||||
t.Fatalf("PgUp did not move the persistent Bypass preview viewport:\n%s", view)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteModelProfileEditorObscuresCredentialsInScrolledBypassValidation(t *testing.T) {
|
||||
module := &profileManagementModule{}
|
||||
model := openNewProfileEditor(t, module)
|
||||
model = updateRunes(model, "office")
|
||||
model = updateKey(model, "tab")
|
||||
model = updateRunes(model, "proxy.example:8080")
|
||||
model = updateKey(model, "tab")
|
||||
items := make([]string, 0, 25)
|
||||
for i := range 24 {
|
||||
items = append(items, fmt.Sprintf("host-%02d.example", i))
|
||||
}
|
||||
const secret = "late-bypass-secret"
|
||||
items = append(items, "http://operator:"+secret+"@proxy.invalid")
|
||||
model = updateRunes(model, strings.Join(items, ","))
|
||||
for range 6 {
|
||||
model = updateKey(model, "pgdown")
|
||||
}
|
||||
|
||||
for _, size := range []struct{ width, height int }{{80, 24}, {60, 16}, {120, 36}, {80, 24}} {
|
||||
model = resize(model, size.width, size.height)
|
||||
view := model.View()
|
||||
assertUsableFrame(t, view, size.width, size.height, "> Bypass [focused]", "REDACTED@proxy.invalid")
|
||||
if strings.Contains(view, secret) {
|
||||
t.Fatalf("scrolled Bypass validation exposed credentials at %dx%d:\n%s", size.width, size.height, view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteModelProfileEditorObscuresSchemeLessCredentialsDuringEntryValidationAndScroll(t *testing.T) {
|
||||
module := &profileManagementModule{}
|
||||
model := openNewProfileEditor(t, module)
|
||||
model = updateRunes(model, "office")
|
||||
model = updateKey(model, "tab")
|
||||
const secret = "scheme-less-secret"
|
||||
const raw = "operator:" + secret + "@this-is-a-very-long-proxy-hostname-for-scroll.example:notaport"
|
||||
model = updateRunes(model, raw)
|
||||
|
||||
for _, step := range []func(Model) Model{
|
||||
func(value Model) Model { return value },
|
||||
func(value Model) Model { return editorKey(value, tea.KeyHome) },
|
||||
func(value Model) Model { return editorKey(value, tea.KeyEnd) },
|
||||
func(value Model) Model { return updateKey(value, "ctrl+s") },
|
||||
func(value Model) Model { return resize(value, 60, 16) },
|
||||
func(value Model) Model { return resize(value, 80, 24) },
|
||||
} {
|
||||
model = step(model)
|
||||
if view := model.View(); strings.Contains(view, secret) {
|
||||
t.Fatalf("scheme-less credential rendered during entry, validation, or scroll:\n%s", view)
|
||||
}
|
||||
}
|
||||
if len(module.intents) != 0 {
|
||||
t.Fatalf("invalid credential-bearing URL dispatched %#v", module.intents)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteModelProfileEditorShowsNameConflictsInlineAndBlocksContradictorySave(t *testing.T) {
|
||||
|
||||
@@ -27,7 +27,7 @@ type formState struct {
|
||||
profile profile.Profile
|
||||
inputs []formInput
|
||||
field int
|
||||
viewportOffset int
|
||||
bypassPreviewLine int
|
||||
validation profile.Validation
|
||||
advancedInitialized bool
|
||||
probing bool
|
||||
@@ -48,7 +48,7 @@ func newFormState(originalName string, value profile.Profile) formState {
|
||||
}
|
||||
|
||||
func (f *formState) rebuildInputs(focus int) tea.Cmd {
|
||||
f.viewportOffset = 0
|
||||
f.bypassPreviewLine = 0
|
||||
definitions := []formDefinition{{"Name", profile.NameField, f.profile.Name}}
|
||||
if f.profile.Mode == profile.AdvancedMode {
|
||||
definitions = append(definitions,
|
||||
@@ -95,7 +95,7 @@ func (f *formState) move(delta int) tea.Cmd {
|
||||
}
|
||||
f.inputs[f.field].input.Blur()
|
||||
f.field = (f.field + delta + len(f.inputs)) % len(f.inputs)
|
||||
f.viewportOffset = 0
|
||||
f.bypassPreviewLine = 0
|
||||
return f.inputs[f.field].input.Focus()
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ func (f *formState) updateInput(key tea.KeyMsg) tea.Cmd {
|
||||
commands = append(commands, command)
|
||||
}
|
||||
if f.inputs[f.field].input.Value() != previous {
|
||||
f.viewportOffset = 0
|
||||
f.bypassPreviewLine = 0
|
||||
f.syncFocusedValue()
|
||||
f.refreshValidation()
|
||||
}
|
||||
@@ -181,6 +181,17 @@ func (f formState) currentField() profile.Field {
|
||||
return f.inputs[f.field].field
|
||||
}
|
||||
|
||||
func (f *formState) scrollBypassPreview(delta int) {
|
||||
if f.currentField() != profile.BypassField {
|
||||
return
|
||||
}
|
||||
// Line zero is the preview heading; each following line represents one
|
||||
// normalized or invalid validation item. Anchoring the viewport to that
|
||||
// item keeps Page navigation stable across terminal resizes.
|
||||
maximum := len(f.bypassAnalysis())
|
||||
f.bypassPreviewLine = min(maximum, max(0, f.bypassPreviewLine+delta))
|
||||
}
|
||||
|
||||
func (f formState) renderedInput(index, width int) string {
|
||||
if index < 0 || index >= len(f.inputs) {
|
||||
return ""
|
||||
|
||||
Reference in New Issue
Block a user