aboutsummaryrefslogtreecommitdiff
path: root/app/ui/tui.py
diff options
context:
space:
mode:
Diffstat (limited to 'app/ui/tui.py')
-rw-r--r--app/ui/tui.py42
1 files changed, 42 insertions, 0 deletions
diff --git a/app/ui/tui.py b/app/ui/tui.py
index 95130cf..0a119e9 100644
--- a/app/ui/tui.py
+++ b/app/ui/tui.py
@@ -46,6 +46,48 @@ class WizardCancelled(Exception):
"""Raised when the user presses Esc to abort the wizard."""
+class Wizard:
+ """Drive a stack of screen closures with "Esc goes back one screen".
+
+ Each screen is a zero-argument callable that shows exactly one
+ interactive screen and returns a navigation result:
+
+ Wizard.BACK the user pressed Esc/q; go back one screen
+ a callable advance to that screen (it is the next screen)
+ None abort the whole wizard
+ any value finish the wizard and return that value (the settings)
+
+ ``run(first_screen)`` returns the final value, or None when the user
+ pressed Esc on the first screen (or a screen returned None). Only
+ screens that actually render are pushed onto the stack, so Esc always
+ lands on the previous real screen; a step whose value is already known
+ (a flag, or a condition that does not apply) is folded into the screen
+ that precedes it and never appears on the stack, so it cannot be backed
+ into.
+ """
+
+ BACK = object()
+
+ def __init__(self):
+ self._stack = []
+
+ def run(self, first_screen) -> Optional[object]:
+ screen = first_screen
+ while True:
+ nxt = screen()
+ if nxt is Wizard.BACK:
+ if not self._stack:
+ return None
+ screen = self._stack.pop()
+ continue
+ if nxt is None:
+ return None
+ if not callable(nxt):
+ return nxt
+ self._stack.append(screen)
+ screen = nxt
+
+
@contextlib.contextmanager
def suspend(scr):
"""Temporarily leave curses to run plain-console code.