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:
Vlad Doloman
2026-06-24 13:53:20 +03:00
parent 567c3e9759
commit 6ed7f9feef
2 changed files with 65 additions and 28 deletions

View File

@@ -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)
# ============================================================ # ============================================================

View File

@@ -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