feat: add Continue and Cancel buttons to Certificate Details form
Tab (or Down/Enter on a field) now cycles through the three text fields and then onto [ Continue ] and [ Cancel ] buttons at the bottom of the form. Enter or Space activates the focused button. Ctrl-G still confirms immediately from any position. Escape still cancels. The form window is trimmed from h=16 to h=13 since the buttons replace the now-redundant empty space below the password field. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -567,15 +567,22 @@ def show_cert_form(
|
|||||||
cn_readonly: bool = False,
|
cn_readonly: bool = False,
|
||||||
) -> CertFormResult:
|
) -> CertFormResult:
|
||||||
"""Modal cert-detail form.
|
"""Modal cert-detail form.
|
||||||
Tab / Shift-Tab / Up / Down cycle fields. F5 regenerates password.
|
Tab / Shift-Tab / Up / Down cycle fields and buttons. F5 regenerates password.
|
||||||
Enter on last field or Ctrl-S confirms. Escape cancels."""
|
Enter or Space activates the focused button. Ctrl-G confirms immediately. Escape cancels."""
|
||||||
sh, sw = stdscr.getmaxyx()
|
sh, sw = stdscr.getmaxyx()
|
||||||
h, w = 16, 62
|
h, w = 13, 62
|
||||||
win = curses.newwin(h, w, (sh - h) // 2, (sw - w) // 2)
|
win = curses.newwin(h, w, (sh - h) // 2, (sw - w) // 2)
|
||||||
win.keypad(True)
|
win.keypad(True)
|
||||||
fw = w - 6 # field width
|
fw = w - 6 # field width
|
||||||
|
|
||||||
active = [n for n in _FORM_FIELDS if not (n == "cn" and cn_readonly)]
|
_BTN_CONTINUE = "_continue"
|
||||||
|
_BTN_CANCEL = "_cancel"
|
||||||
|
_BTN_Y = 9
|
||||||
|
_btn_x_cont = (w - 26) // 2 # "[ Continue ]"=12 + gap=4 + "[ Cancel ]"=10 = 26
|
||||||
|
_btn_x_canc = _btn_x_cont + 16
|
||||||
|
|
||||||
|
active = ([n for n in _FORM_FIELDS if not (n == "cn" and cn_readonly)]
|
||||||
|
+ [_BTN_CONTINUE, _BTN_CANCEL])
|
||||||
fields: dict[str, InputField] = {
|
fields: dict[str, InputField] = {
|
||||||
"cn": InputField(win, _FORM_FIELD_Y["cn"], 3, fw, initial=cn),
|
"cn": InputField(win, _FORM_FIELD_Y["cn"], 3, fw, initial=cn),
|
||||||
"email": InputField(win, _FORM_FIELD_Y["email"], 3, fw, initial=email),
|
"email": InputField(win, _FORM_FIELD_Y["email"], 3, fw, initial=email),
|
||||||
@@ -616,8 +623,33 @@ def show_cert_form(
|
|||||||
else:
|
else:
|
||||||
fields[name].draw()
|
fields[name].draw()
|
||||||
|
|
||||||
fields[active[focus]].place_cursor()
|
current = active[focus]
|
||||||
hint = "Tab=next F5=regen password Ctrl-G=confirm Esc=cancel"
|
for btn_label, btn_id, bx in (
|
||||||
|
("Continue", _BTN_CONTINUE, _btn_x_cont),
|
||||||
|
("Cancel", _BTN_CANCEL, _btn_x_canc),
|
||||||
|
):
|
||||||
|
btn_attr = (curses.color_pair(COLOR_SELECTED)
|
||||||
|
if current == btn_id
|
||||||
|
else curses.color_pair(COLOR_NORMAL))
|
||||||
|
try:
|
||||||
|
win.addstr(_BTN_Y, bx, f"[ {btn_label} ]", btn_attr | curses.A_BOLD)
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if current == _BTN_CONTINUE:
|
||||||
|
try:
|
||||||
|
win.move(_BTN_Y, _btn_x_cont)
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
elif current == _BTN_CANCEL:
|
||||||
|
try:
|
||||||
|
win.move(_BTN_Y, _btn_x_canc)
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
fields[current].place_cursor()
|
||||||
|
|
||||||
|
hint = "Tab=next F5=regen pwd Ctrl-G=confirm Esc=cancel"
|
||||||
try:
|
try:
|
||||||
win.addstr(h - 2, 2, hint[:w - 4], curses.color_pair(COLOR_DISABLED))
|
win.addstr(h - 2, 2, hint[:w - 4], curses.color_pair(COLOR_DISABLED))
|
||||||
except curses.error:
|
except curses.error:
|
||||||
@@ -632,16 +664,10 @@ def show_cert_form(
|
|||||||
if key == 0x07: # Ctrl-G: immediate submit
|
if key == 0x07: # Ctrl-G: immediate submit
|
||||||
curses.curs_set(0)
|
curses.curs_set(0)
|
||||||
return _submit()
|
return _submit()
|
||||||
if key == 9: # Tab
|
if key in (9, curses.KEY_DOWN): # Tab / Down: next
|
||||||
focus = (focus + 1) % len(active)
|
focus = (focus + 1) % len(active)
|
||||||
continue
|
continue
|
||||||
if key == curses.KEY_BTAB: # Shift-Tab
|
if key in (curses.KEY_BTAB, curses.KEY_UP): # Shift-Tab / Up: prev
|
||||||
focus = (focus - 1) % len(active)
|
|
||||||
continue
|
|
||||||
if key == curses.KEY_DOWN:
|
|
||||||
focus = (focus + 1) % len(active)
|
|
||||||
continue
|
|
||||||
if key == curses.KEY_UP:
|
|
||||||
focus = (focus - 1) % len(active)
|
focus = (focus - 1) % len(active)
|
||||||
continue
|
continue
|
||||||
if key == curses.KEY_F5:
|
if key == curses.KEY_F5:
|
||||||
@@ -650,13 +676,17 @@ def show_cert_form(
|
|||||||
initial=generate_password(), mask=True,
|
initial=generate_password(), mask=True,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
if key in (10, 13, curses.KEY_ENTER):
|
if current in (_BTN_CONTINUE, _BTN_CANCEL):
|
||||||
if focus < len(active) - 1:
|
if key in (10, 13, curses.KEY_ENTER, ord(" ")):
|
||||||
focus += 1
|
curses.curs_set(0)
|
||||||
|
if current == _BTN_CONTINUE:
|
||||||
|
return _submit()
|
||||||
|
return CertFormResult(cn=cn, email="", password="", confirmed=False)
|
||||||
|
else:
|
||||||
|
if key in (10, 13, curses.KEY_ENTER):
|
||||||
|
focus = (focus + 1) % len(active)
|
||||||
continue
|
continue
|
||||||
curses.curs_set(0)
|
fields[current].handle_key(key)
|
||||||
return _submit()
|
|
||||||
fields[active[focus]].handle_key(key)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
@@ -78,16 +78,11 @@ def test_show_cert_form_cancel():
|
|||||||
|
|
||||||
|
|
||||||
def test_show_cert_form_confirm():
|
def test_show_cert_form_confirm():
|
||||||
"""Pressing Enter three times advances through all 3 fields and confirms.
|
"""Enter advances through the 3 fields to the Continue button; Enter on
|
||||||
|
Continue confirms. That's 4 Enter keypresses total."""
|
||||||
The form has 3 active fields (cn, email, password). Enter on field 0
|
|
||||||
advances to field 1; Enter on field 1 advances to field 2; Enter on
|
|
||||||
field 2 (the last) confirms. We therefore need 3 Enter keypresses.
|
|
||||||
"""
|
|
||||||
stdscr = _make_stdscr()
|
stdscr = _make_stdscr()
|
||||||
win = _make_win(rows=20, cols=70)
|
win = _make_win(rows=20, cols=70)
|
||||||
# Three Enter keypresses to advance through each field and confirm.
|
win.getch.side_effect = [10, 10, 10, 10]
|
||||||
win.getch.side_effect = [10, 10, 10]
|
|
||||||
with patch("curses.newwin", return_value=win):
|
with patch("curses.newwin", return_value=win):
|
||||||
result = show_cert_form(stdscr, cn="bob", email="bob@example.com")
|
result = show_cert_form(stdscr, cn="bob", email="bob@example.com")
|
||||||
assert isinstance(result, CertFormResult)
|
assert isinstance(result, CertFormResult)
|
||||||
@@ -95,3 +90,15 @@ def test_show_cert_form_confirm():
|
|||||||
assert result.cn == "bob"
|
assert result.cn == "bob"
|
||||||
assert result.email == "bob@example.com"
|
assert result.email == "bob@example.com"
|
||||||
assert len(result.password) > 0
|
assert len(result.password) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_cert_form_cancel_button():
|
||||||
|
"""Tab to the Cancel button (4 Tabs from start) and press Enter cancels."""
|
||||||
|
stdscr = _make_stdscr()
|
||||||
|
win = _make_win(rows=20, cols=70)
|
||||||
|
# Tab×4: cn→email→password→Continue→Cancel, then Enter
|
||||||
|
win.getch.side_effect = [9, 9, 9, 9, 10]
|
||||||
|
with patch("curses.newwin", return_value=win):
|
||||||
|
result = show_cert_form(stdscr, cn="alice", email="alice@example.com")
|
||||||
|
assert isinstance(result, CertFormResult)
|
||||||
|
assert result.confirmed is False
|
||||||
|
|||||||
Reference in New Issue
Block a user