Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 144 additions & 76 deletions README.md

Large diffs are not rendered by default.

84 changes: 84 additions & 0 deletions internal/ui/globals_visible_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package ui_test

import (
"fmt"
"strings"
"testing"

tea "github.com/charmbracelet/bubbletea"

"github.com/someson/azform/internal/metadata"
"github.com/someson/azform/internal/ui"
)

// globalsFixture builds a form shaped like a real `az` command: a couple
// of required flags, a batch of optional ones, and the seven Global
// Arguments every az command carries.
func globalsFixture(t *testing.T, w, h int) ui.Form {
t.Helper()
ps := []metadata.Parameter{
{Name: "--name", Required: true, TakesValue: true},
{Name: "--resource-group", Required: true, TakesValue: true},
}
for _, n := range []string{
"--allocation-method", "--ddos-protection-mode", "--dns-name",
"--edge-zone", "--idle-timeout", "--ip-address", "--sku",
"--tags", "--tier", "--zone",
} {
ps = append(ps, metadata.Parameter{Name: n, TakesValue: true})
}
for _, n := range []string{"--debug", "--help", "--only-show-errors", "--verbose"} {
ps = append(ps, metadata.Parameter{Name: n, Global: true, Group: "Global Arguments"})
}
for _, n := range []string{"--output", "--query", "--subscription"} {
ps = append(ps, metadata.Parameter{Name: n, Global: true, TakesValue: true, Group: "Global Arguments"})
}
f := ui.NewForm("network public-ip create", "/tmp/out.txt", t.TempDir(), "test", nil)
m, _ := f.Update(ui.MetadataLoadedMsg{Params: ps, Summary: "Create a public IP address."})
f = m.(ui.Form)
m, _ = f.Update(tea.WindowSizeMsg{Width: w, Height: h})
return m.(ui.Form)
}

// TestGlobalsAlwaysInLayout pins the behaviour that replaced the G toggle:
// every Global Argument is part of the layout at every terminal size, with
// no hidden state and no key needed to reveal it. On a short terminal they
// may sit below the fold — that is ordinary scrolling, not hiding, so the
// assertion is on the layout rather than on the rendered window.
//
// The toggle this replaced was a no-op in every configuration measured: on
// a tall terminal an internal "they fit, show them anyway" rule overrode
// it, and on a short or narrow one the section stayed collapsed and G could
// not reveal it. The single-column path even printed "press G to show N
// global argument(s)", advertising a key that did nothing.
func TestGlobalsAlwaysInLayout(t *testing.T) {
t.Parallel()
const wantGlobals = 7
sizes := []struct{ w, h int }{{140, 40}, {106, 30}, {106, 12}, {106, 5}, {80, 30}, {80, 10}}
for _, s := range sizes {
t.Run(fmt.Sprintf("%dx%d", s.w, s.h), func(t *testing.T) {
t.Parallel()
f := globalsFixture(t, s.w, s.h)
_, globalsCol, cols := f.GridLayout()
if cols >= 2 && len(globalsCol) != wantGlobals {
t.Errorf("%dx%d: globalsCol has %d entries, want %d — globals must never be withheld from the layout",
s.w, s.h, len(globalsCol), wantGlobals)
}
if v := f.View(); strings.Contains(v, "press G") {
t.Errorf("%dx%d: view still advertises the removed G toggle", s.w, s.h)
}
})
}
}

// TestGlobalsRenderWhenRoom is the visible counterpart: on a terminal with
// room for them, globals appear without any keypress.
func TestGlobalsRenderWhenRoom(t *testing.T) {
t.Parallel()
v := globalsFixture(t, 140, 40).View()
for _, want := range []string{"--verbose", "--output", "--subscription"} {
if !strings.Contains(v, want) {
t.Errorf("global %s not rendered on a roomy terminal; view:\n%s", want, v)
}
}
}
8 changes: 1 addition & 7 deletions internal/ui/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,17 +261,11 @@ func (m Form) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
// pendingExports; main.go flushes the lines to --env-out on
// Done, and the widget evals them in your interactive zsh
// after azform exits so the var lives in the shell until
// you unset it. The old "toggle Global Arguments" behaviour
// moved to G (shift+g) — see the next case.
// you unset it.
m.setVarInput.SetValue("")
m.clearSetVarHint()
m.mode = FormModeSetVar
return m, m.setVarInput.Focus()
case "G":
// Old 'g' binding (toggle Global Arguments section), shifted
// to uppercase to make room for the set-var popup.
m.showGlobals = !m.showGlobals
return m, nil
case "h", "left":
if m.moveCursorHoriz(-1) {
if idx := m.fieldAt(m.cursor); idx >= 0 {
Expand Down
42 changes: 20 additions & 22 deletions internal/ui/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,28 @@ import "github.com/charmbracelet/bubbles/key"

// KeyMap holds all key bindings for the form (spec 6.6).
type KeyMap struct {
Up key.Binding
Down key.Binding
Toggle key.Binding // Space: enable/disable param
Edit key.Binding // Enter in list: open value editor
Filter key.Binding // /: open filter
Tab key.Binding
ShiftTab key.Binding
Confirm key.Binding // Enter on Done button
Quit key.Binding // Esc/q in list: exit without result
ShowAll key.Binding // a: expand collapsed params
ShowGlobal key.Binding // G (shift+g): show global args
Up key.Binding
Down key.Binding
Toggle key.Binding // Space: enable/disable param
Edit key.Binding // Enter in list: open value editor
Filter key.Binding // /: open filter
Tab key.Binding
ShiftTab key.Binding
Confirm key.Binding // Enter on Done button
Quit key.Binding // Esc/q in list: exit without result
ShowAll key.Binding // a: expand collapsed params
}

// DefaultKeys matches the keyboard layout in spec 6.6.
var DefaultKeys = KeyMap{
Up: key.NewBinding(key.WithKeys("up", "k")),
Down: key.NewBinding(key.WithKeys("down", "j")),
Toggle: key.NewBinding(key.WithKeys(" ")),
Edit: key.NewBinding(key.WithKeys("enter")),
Filter: key.NewBinding(key.WithKeys("/")),
Tab: key.NewBinding(key.WithKeys("tab")),
ShiftTab: key.NewBinding(key.WithKeys("shift+tab")),
Confirm: key.NewBinding(key.WithKeys("enter")),
Quit: key.NewBinding(key.WithKeys("esc", "q")),
ShowAll: key.NewBinding(key.WithKeys("a")),
ShowGlobal: key.NewBinding(key.WithKeys("G")),
Up: key.NewBinding(key.WithKeys("up", "k")),
Down: key.NewBinding(key.WithKeys("down", "j")),
Toggle: key.NewBinding(key.WithKeys(" ")),
Edit: key.NewBinding(key.WithKeys("enter")),
Filter: key.NewBinding(key.WithKeys("/")),
Tab: key.NewBinding(key.WithKeys("tab")),
ShiftTab: key.NewBinding(key.WithKeys("shift+tab")),
Confirm: key.NewBinding(key.WithKeys("enter")),
Quit: key.NewBinding(key.WithKeys("esc", "q")),
ShowAll: key.NewBinding(key.WithKeys("a")),
}
43 changes: 4 additions & 39 deletions internal/ui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,6 @@ type Form struct {
draftStore *state.DraftStore
draftRestored bool

// showGlobals controls whether Azure CLI "Global Arguments" (--output,
// --query, --subscription, --verbose, --debug, etc.) are rendered in the
// optional list. Toggle with the 'g' key. Enabled globals always appear
// in the built command regardless of this flag.
showGlobals bool

staleWarn string
quitting bool
result string
Expand Down Expand Up @@ -802,9 +796,6 @@ func (m *Form) widestName() int {
w := gridMinNameCol
for _, idx := range m.visible {
f := &m.fields[idx]
if f.Param.Global && !m.showGlobals && !f.Enabled {
continue
}
if n := len(f.Param.Name); n > w {
w = n
}
Expand All @@ -831,9 +822,8 @@ func (m *Form) gridCellWidth() int {
// Returns:
// - roCols: Req+Opt field indices split across 1 or 2 columns
// (required-first order preserved).
// - globalsCol: global field indices for the last column; empty when
// showGlobals is off and no globals are enabled AND there are no globals
// to hint about.
// - globalsCol: global field indices for the last column; empty only when
// the command has no global parameters.
// - cols: total column count (1, 2, or 3). Callers use cols == 1 to fall
// back to the single-column render path.
//
Expand All @@ -845,34 +835,12 @@ func (m *Form) gridLayout() (roCols [][]int, globalsCol []int, cols int) {
return nil, nil, 1
}

// Count total globals to decide if they all fit vertically. When they do,
// show them all unconditionally (a dedicated column with room to spare has
// no reason to hide anything). When they don't fit, respect the 'g' toggle
// so users can collapse them to save space.
totalGlobals := 0
for _, idx := range m.visible {
if m.fields[idx].Param.Global {
totalGlobals++
}
}
availableRows := 20 // reasonable default before the first WindowSizeMsg
if m.vpReady && m.vp.Height > 0 {
availableRows = m.vp.Height
}
fitAll := totalGlobals <= availableRows
showAllGlobals := m.showGlobals || fitAll

// Partition visible fields into (req+opt) and (globals).
var ro []int
var hasHiddenGlobal bool
for _, idx := range m.visible {
f := &m.fields[idx]
if f.Param.Global {
if showAllGlobals || f.Enabled {
globalsCol = append(globalsCol, idx)
} else {
hasHiddenGlobal = true
}
globalsCol = append(globalsCol, idx)
continue
}
ro = append(ro, idx)
Expand All @@ -887,7 +855,7 @@ func (m *Form) gridLayout() (roCols [][]int, globalsCol []int, cols int) {

// A globals column is "present" (occupies a slot) whenever the command
// has any global arguments — visible or hidden-but-hintable.
globalPresent := len(globalsCol) > 0 || hasHiddenGlobal
globalPresent := len(globalsCol) > 0

// Req+Opt: 1 col unless count > threshold AND we have room for 2 ro cols
// plus (if needed) the globals col.
Expand Down Expand Up @@ -1073,9 +1041,6 @@ func (m Form) Visible() []int { return append([]int(nil), m.visible...) }
// DraftRestored reports whether NewForm loaded a persisted draft.
func (m Form) DraftRestored() bool { return m.draftRestored }

// ShowGlobals reports whether the "g" toggle is showing global params.
func (m Form) ShowGlobals() bool { return m.showGlobals }

// Quitting reports whether the form has signalled tea.Quit.
func (m Form) Quitting() bool { return m.quitting }

Expand Down
Loading