diff --git a/openvpncertupdate.py b/openvpncertupdate.py index 3d83855..0f4a3b0 100644 --- a/openvpncertupdate.py +++ b/openvpncertupdate.py @@ -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) # ============================================================ diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index 6a8a2ef..2b9a8df 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -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