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,
|
||||
) -> CertFormResult:
|
||||
"""Modal cert-detail form.
|
||||
Tab / Shift-Tab / Up / Down cycle fields. F5 regenerates password.
|
||||
Enter on last field or Ctrl-S confirms. Escape cancels."""
|
||||
Tab / Shift-Tab / Up / Down cycle fields and buttons. F5 regenerates password.
|
||||
Enter or Space activates the focused button. Ctrl-G confirms immediately. Escape cancels."""
|
||||
sh, sw = stdscr.getmaxyx()
|
||||
h, w = 16, 62
|
||||
h, w = 13, 62
|
||||
win = curses.newwin(h, w, (sh - h) // 2, (sw - w) // 2)
|
||||
win.keypad(True)
|
||||
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] = {
|
||||
"cn": InputField(win, _FORM_FIELD_Y["cn"], 3, fw, initial=cn),
|
||||
"email": InputField(win, _FORM_FIELD_Y["email"], 3, fw, initial=email),
|
||||
@@ -616,8 +623,33 @@ def show_cert_form(
|
||||
else:
|
||||
fields[name].draw()
|
||||
|
||||
fields[active[focus]].place_cursor()
|
||||
hint = "Tab=next F5=regen password Ctrl-G=confirm Esc=cancel"
|
||||
current = active[focus]
|
||||
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:
|
||||
win.addstr(h - 2, 2, hint[:w - 4], curses.color_pair(COLOR_DISABLED))
|
||||
except curses.error:
|
||||
@@ -632,16 +664,10 @@ def show_cert_form(
|
||||
if key == 0x07: # Ctrl-G: immediate submit
|
||||
curses.curs_set(0)
|
||||
return _submit()
|
||||
if key == 9: # Tab
|
||||
if key in (9, curses.KEY_DOWN): # Tab / Down: next
|
||||
focus = (focus + 1) % len(active)
|
||||
continue
|
||||
if key == curses.KEY_BTAB: # Shift-Tab
|
||||
focus = (focus - 1) % len(active)
|
||||
continue
|
||||
if key == curses.KEY_DOWN:
|
||||
focus = (focus + 1) % len(active)
|
||||
continue
|
||||
if key == curses.KEY_UP:
|
||||
if key in (curses.KEY_BTAB, curses.KEY_UP): # Shift-Tab / Up: prev
|
||||
focus = (focus - 1) % len(active)
|
||||
continue
|
||||
if key == curses.KEY_F5:
|
||||
@@ -650,13 +676,17 @@ def show_cert_form(
|
||||
initial=generate_password(), mask=True,
|
||||
)
|
||||
continue
|
||||
if key in (10, 13, curses.KEY_ENTER):
|
||||
if focus < len(active) - 1:
|
||||
focus += 1
|
||||
if current in (_BTN_CONTINUE, _BTN_CANCEL):
|
||||
if key in (10, 13, curses.KEY_ENTER, ord(" ")):
|
||||
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
|
||||
curses.curs_set(0)
|
||||
return _submit()
|
||||
fields[active[focus]].handle_key(key)
|
||||
fields[current].handle_key(key)
|
||||
|
||||
|
||||
# ============================================================
|
||||
|
||||
@@ -78,16 +78,11 @@ def test_show_cert_form_cancel():
|
||||
|
||||
|
||||
def test_show_cert_form_confirm():
|
||||
"""Pressing Enter three times advances through all 3 fields and confirms.
|
||||
|
||||
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.
|
||||
"""
|
||||
"""Enter advances through the 3 fields to the Continue button; Enter on
|
||||
Continue confirms. That's 4 Enter keypresses total."""
|
||||
stdscr = _make_stdscr()
|
||||
win = _make_win(rows=20, cols=70)
|
||||
# Three Enter keypresses to advance through each field and confirm.
|
||||
win.getch.side_effect = [10, 10, 10]
|
||||
win.getch.side_effect = [10, 10, 10, 10]
|
||||
with patch("curses.newwin", return_value=win):
|
||||
result = show_cert_form(stdscr, cn="bob", email="bob@example.com")
|
||||
assert isinstance(result, CertFormResult)
|
||||
@@ -95,3 +90,15 @@ def test_show_cert_form_confirm():
|
||||
assert result.cn == "bob"
|
||||
assert result.email == "bob@example.com"
|
||||
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