feat: unify tui editor screens
This commit is contained in:
parent
18d7a7c07a
commit
5e83300ea8
|
|
@ -342,8 +342,8 @@ func TestAuthMethodListViewShowsAllOptions(t *testing.T) {
|
|||
if between := view[authPos:listPos]; strings.Contains(between, "Identity File") {
|
||||
t.Fatalf("expected auth method list to render directly under auth field\nview:\n%s", view)
|
||||
}
|
||||
if strings.Contains(view, "│") {
|
||||
t.Fatalf("expected compact auth method dropdown without default list border\nview:\n%s", view)
|
||||
if !strings.Contains(view, "│") {
|
||||
t.Fatalf("expected auth method dropdown inside the unified frame\nview:\n%s", view)
|
||||
}
|
||||
for _, method := range []model.AuthMethod{
|
||||
model.AuthPassword,
|
||||
|
|
@ -383,8 +383,8 @@ func TestGroupListViewRendersDirectlyUnderGroupField(t *testing.T) {
|
|||
if between := view[groupPos:listPos]; strings.Contains(between, "Password") {
|
||||
t.Fatalf("expected group dropdown to render before password field\nview:\n%s", view)
|
||||
}
|
||||
if strings.Contains(view, "│") {
|
||||
t.Fatalf("expected compact group dropdown without default list border\nview:\n%s", view)
|
||||
if !strings.Contains(view, "│") {
|
||||
t.Fatalf("expected group dropdown inside the unified frame\nview:\n%s", view)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -589,14 +589,6 @@ func (fm *formModel) View() string {
|
|||
if fm.edit {
|
||||
title = "Edit Server: " + fm.server.Alias
|
||||
}
|
||||
footer := renderHelp([]helpItem{
|
||||
{Key: "Tab/↓", Action: "next"},
|
||||
{Key: "↑", Action: "prev"},
|
||||
{Key: "/", Action: "pick list"},
|
||||
{Key: "Enter", Action: "select"},
|
||||
{Key: "Esc", Action: "back"},
|
||||
}, fm.width)
|
||||
|
||||
if fm.showAuthList || fm.showGroupList {
|
||||
var dropdown list.Model
|
||||
fieldIndex := 8
|
||||
|
|
@ -606,10 +598,18 @@ func (fm *formModel) View() string {
|
|||
} else {
|
||||
dropdown = fm.groupList
|
||||
}
|
||||
return titleStyle.Copy().MarginLeft(0).Render(fitLine(title, fm.width)) + "\n" +
|
||||
fitLine(fm.inputs[fieldIndex].View(), fm.width) + "\n" +
|
||||
fitLine(renderDropdown(dropdown), fm.width) + "\n" +
|
||||
renderHelp([]helpItem{{Key: "Enter", Action: "select"}, {Key: "Esc", Action: "cancel"}}, fm.width)
|
||||
return renderScreenShell(screenShell{
|
||||
breadcrumb: title + " / Picker",
|
||||
status: "Choose a value",
|
||||
width: fm.width,
|
||||
height: fm.height,
|
||||
body: func(width, height int) string {
|
||||
lines := []string{fm.inputs[fieldIndex].View(), ""}
|
||||
lines = append(lines, splitBlock(renderDropdown(dropdown))...)
|
||||
return renderPaddedPanel(width, height, lines)
|
||||
},
|
||||
footer: []helpItem{{Key: "↑/↓", Action: "move"}, {Key: "Enter", Action: "select"}, {Key: "Ctrl+H", Action: "help"}, {Key: "Esc", Action: "cancel"}},
|
||||
})
|
||||
}
|
||||
|
||||
status := fm.formStatusLine()
|
||||
|
|
@ -620,60 +620,68 @@ func (fm *formModel) View() string {
|
|||
if fm.focusIdx == len(fm.inputs)+2 {
|
||||
saveBtn = selectedStyle.Render("> [ Save ]")
|
||||
}
|
||||
actions := fitLine(testBtn+" "+saveBtn, fm.width)
|
||||
actions := testBtn + " " + saveBtn
|
||||
|
||||
reserved := 1 + displayLineCount(footer) + 1
|
||||
if status != "" {
|
||||
reserved++
|
||||
}
|
||||
fieldRows := max(4, fm.height-reserved)
|
||||
richLayout := fm.width >= 90 && fm.height >= 24
|
||||
allFields := make([]string, 0, len(fm.inputs)+5)
|
||||
focusRows := make([]int, len(fm.inputs)+1)
|
||||
for i := range fm.inputs {
|
||||
if richLayout {
|
||||
if section := formSectionTitle(i); section != "" {
|
||||
allFields = append(allFields, sectionStyle.Copy().MarginTop(0).Render(section))
|
||||
body := func(width, height int) string {
|
||||
richLayout := width >= 90 && height >= 20
|
||||
allFields := make([]string, 0, len(fm.inputs)+5)
|
||||
focusRows := make([]int, len(fm.inputs)+1)
|
||||
for i := range fm.inputs {
|
||||
if richLayout {
|
||||
if section := formSectionTitle(i); section != "" {
|
||||
allFields = append(allFields, sectionStyle.Copy().MarginTop(0).Render(section))
|
||||
}
|
||||
}
|
||||
if i == 5 {
|
||||
fm.inputs[i].Placeholder = "password/key/key_passphrase/agent"
|
||||
}
|
||||
if i == 8 && len(fm.groups) > 0 {
|
||||
fm.inputs[i].Placeholder = truncateCells(strings.Join(fm.groups, ", "), 25)
|
||||
}
|
||||
focusRows[i] = len(allFields)
|
||||
allFields = append(allFields, fm.inputs[i].View())
|
||||
}
|
||||
if i == 5 {
|
||||
fm.inputs[i].Placeholder = "password/key/key_passphrase/agent"
|
||||
focusRows[len(fm.inputs)] = len(allFields)
|
||||
allFields = append(allFields, fm.password.View())
|
||||
focusField := len(allFields) - 1
|
||||
if fm.focusIdx <= len(fm.inputs) {
|
||||
focusField = focusRows[fm.focusIdx]
|
||||
}
|
||||
if i == 8 && len(fm.groups) > 0 {
|
||||
fm.inputs[i].Placeholder = truncateCells(strings.Join(fm.groups, ", "), 25)
|
||||
actionRows := 1
|
||||
if richLayout {
|
||||
actionRows = 2
|
||||
}
|
||||
focusRows[i] = len(allFields)
|
||||
allFields = append(allFields, fitLine(fm.inputs[i].View(), fm.width))
|
||||
fieldRows := max(1, height-2-actionRows)
|
||||
start, end := visibleServerRange(len(allFields), focusField, fieldRows)
|
||||
visible := append([]string(nil), allFields[start:end]...)
|
||||
if start > 0 && len(visible) > 0 {
|
||||
visible[0] = "↑ more fields · " + visible[0]
|
||||
}
|
||||
if end < len(allFields) && len(visible) > 0 {
|
||||
visible[len(visible)-1] += " · more ↓"
|
||||
}
|
||||
if richLayout {
|
||||
visible = append(visible, sectionStyle.Copy().MarginTop(0).Render("Actions"))
|
||||
}
|
||||
visible = append(visible, actions)
|
||||
return renderPaddedPanel(width, height, visible)
|
||||
}
|
||||
focusRows[len(fm.inputs)] = len(allFields)
|
||||
allFields = append(allFields, fitLine(fm.password.View(), fm.width))
|
||||
focusField := len(allFields) - 1
|
||||
if fm.focusIdx <= len(fm.inputs) {
|
||||
focusField = focusRows[fm.focusIdx]
|
||||
}
|
||||
start, end := visibleServerRange(len(allFields), focusField, fieldRows)
|
||||
visible := append([]string(nil), allFields[start:end]...)
|
||||
if start > 0 && len(visible) > 0 {
|
||||
visible[0] = fitLine("↑ more fields · "+visible[0], fm.width)
|
||||
}
|
||||
if end < len(allFields) && len(visible) > 0 {
|
||||
visible[len(visible)-1] = fitLine(visible[len(visible)-1]+" · more ↓", fm.width)
|
||||
}
|
||||
|
||||
lines := []string{titleStyle.Copy().MarginLeft(0).Render(fitLine(title, fm.width))}
|
||||
lines = append(lines, visible...)
|
||||
if status != "" {
|
||||
lines = append(lines, fitLine(status, fm.width))
|
||||
}
|
||||
if richLayout {
|
||||
lines = append(lines, sectionStyle.Copy().MarginTop(0).Render("Actions"))
|
||||
}
|
||||
lines = append(lines, actions)
|
||||
lines = append(lines, strings.Split(footer, "\n")...)
|
||||
if len(lines) > fm.height && fm.height > 0 {
|
||||
lines = lines[:fm.height]
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
return renderScreenShell(screenShell{
|
||||
breadcrumb: title,
|
||||
status: "Server profile",
|
||||
notification: status,
|
||||
width: fm.width,
|
||||
height: fm.height,
|
||||
body: body,
|
||||
footer: []helpItem{
|
||||
{Key: "Tab/↓", Action: "next"},
|
||||
{Key: "↑", Action: "prev"},
|
||||
{Key: "/", Action: "pick list"},
|
||||
{Key: "Enter", Action: "select"},
|
||||
{Key: "Ctrl+H", Action: "help"},
|
||||
{Key: "Esc", Action: "back"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (fm *formModel) formStatusLine() string {
|
||||
|
|
@ -701,7 +709,7 @@ func (fm *formModel) formStatusLine() string {
|
|||
|
||||
func renderDropdown(l list.Model) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(sectionStyle.Render(l.Title))
|
||||
b.WriteString(dashboardSection(l.Title))
|
||||
b.WriteString("\n")
|
||||
for i, item := range l.Items() {
|
||||
group, ok := item.(groupItem)
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ func TestServerFormFitsSupportedTerminalSizes(t *testing.T) {
|
|||
fm.updateFocus()
|
||||
view := fm.View()
|
||||
assertViewFits(t, view, size.width, size.height)
|
||||
assertUnifiedScreen(t, view, size.width, size.height)
|
||||
for _, want := range []string{"Server", "Port *", "not-a-port", "Port must be", "Save", "Esc"} {
|
||||
if !strings.Contains(view, want) {
|
||||
t.Fatalf("form at %dx%d missing %q:\n%s", size.width, size.height, want, view)
|
||||
|
|
@ -224,6 +225,7 @@ func TestTemplateFormFitsSupportedTerminalSizes(t *testing.T) {
|
|||
form.inputs[1].SetValue("printf 'a very long command that remains editable'")
|
||||
view := form.View()
|
||||
assertViewFits(t, view, size.width, size.height)
|
||||
assertUnifiedScreen(t, view, size.width, size.height)
|
||||
for _, want := range []string{"Template", "Name *", "Save", "Esc"} {
|
||||
if !strings.Contains(view, want) {
|
||||
t.Fatalf("template form at %dx%d missing %q:\n%s", size.width, size.height, want, view)
|
||||
|
|
@ -232,6 +234,21 @@ func TestTemplateFormFitsSupportedTerminalSizes(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestServerFormDropdownUsesUnifiedShell(t *testing.T) {
|
||||
for _, size := range []struct{ width, height int }{{120, 40}, {80, 24}, {60, 16}} {
|
||||
form := newFormModel(size.width, size.height)
|
||||
form.focusIdx = 5
|
||||
form.showAuthList = true
|
||||
view := form.View()
|
||||
assertUnifiedScreen(t, view, size.width, size.height)
|
||||
for _, want := range []string{"Select auth method", "password", "agent", "Enter", "Esc"} {
|
||||
if !strings.Contains(view, want) {
|
||||
t.Fatalf("dropdown at %dx%d missing %q:\n%s", size.width, size.height, want, view)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertViewFits(t *testing.T, view string, width, height int) {
|
||||
t.Helper()
|
||||
lines := strings.Split(strings.TrimRight(view, "\n"), "\n")
|
||||
|
|
|
|||
|
|
@ -151,31 +151,40 @@ func (tf *templateFormModel) save() tea.Cmd {
|
|||
}
|
||||
|
||||
func (tf *templateFormModel) View() string {
|
||||
var b strings.Builder
|
||||
title := "Add Template"
|
||||
if tf.edit {
|
||||
title = "Edit Template"
|
||||
}
|
||||
b.WriteString(titleStyle.Copy().MarginLeft(0).Render(fitLine(title, tf.width)))
|
||||
b.WriteString("\n\n")
|
||||
for i := range tf.inputs {
|
||||
b.WriteString(fitLine(tf.inputs[i].View(), tf.width))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
button := " [ Save ]"
|
||||
if tf.focusIdx == len(tf.inputs) {
|
||||
button = selectedStyle.Render("> [ Save ]")
|
||||
}
|
||||
b.WriteString("\n" + button + "\n\n")
|
||||
notification := ""
|
||||
if tf.err != nil {
|
||||
b.WriteString(errorStyle.Render(tf.err.Error()))
|
||||
b.WriteString("\n")
|
||||
notification = errorStyle.Render(tf.err.Error())
|
||||
} else if tf.saved {
|
||||
notification = successStyle.Render("✓ Saved.")
|
||||
}
|
||||
b.WriteString(renderHelp([]helpItem{
|
||||
{Key: "Tab/↓", Action: "next"},
|
||||
{Key: "↑", Action: "prev"},
|
||||
{Key: "Enter", Action: "select"},
|
||||
{Key: "Esc", Action: "back"},
|
||||
}, tf.width))
|
||||
return b.String()
|
||||
return renderScreenShell(screenShell{
|
||||
breadcrumb: "Command Templates / " + title,
|
||||
status: "Template editor",
|
||||
notification: notification,
|
||||
width: tf.width,
|
||||
height: tf.height,
|
||||
body: func(width, height int) string {
|
||||
lines := make([]string, 0, len(tf.inputs)+3)
|
||||
for i := range tf.inputs {
|
||||
lines = append(lines, tf.inputs[i].View())
|
||||
}
|
||||
button := " [ Save ]"
|
||||
if tf.focusIdx == len(tf.inputs) {
|
||||
button = selectedStyle.Render("> [ Save ]")
|
||||
}
|
||||
lines = append(lines, "", button)
|
||||
return renderPaddedPanel(width, height, lines)
|
||||
},
|
||||
footer: []helpItem{
|
||||
{Key: "Tab/↓", Action: "next"},
|
||||
{Key: "↑", Action: "prev"},
|
||||
{Key: "Enter", Action: "select"},
|
||||
{Key: "Ctrl+H", Action: "help"},
|
||||
{Key: "Esc", Action: "back"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue