26 KiB
Plan: Feather S3 signer interactive UI (splash, main menu, generate, view, enter, working)
Status: ready to implement.
Related firmware: firmware/feather_s3_tft/main/main.c, firmware/feather_s3_tft/main/display.c, firmware/feather_s3_tft/main/display.h, firmware/feather_s3_tft/main/mnemonic.c, firmware/feather_s3_tft/main/mnemonic_wordlist.h, firmware/feather_s3_tft/main/key_derivation.c.
Companion: persistence will be added in a follow-up plan (option 3 from design discussion: PIN-derived encryption of NVS-stored seed).
0. Color palette (global)
Strict 4-color palette for ALL screens (no other colors used):
| Color | Constant | Use |
|---|---|---|
| Black | RGB565_BLACK |
Background everywhere |
| White | RGB565_WHITE |
Default text / active labels |
| Red | RGB565_RED |
Selection / focus / important call-outs (e.g., N_SIGNER title) |
| Dim gray | RGB565_DIM_GRAY (0x4208) |
Disabled / unselectable items, hints, footers |
No yellow, cyan, green, magenta, blue. Existing screens (show_idle_screen, show_approval_prompt_with_caller) get retro-fitted to this palette in §5.4. The RGB565_* constants for unused colors stay defined in display.h (harmless) but the new code never references them.
Selection convention:
- Selected menu item: red text on black background (no inverse-video fill needed in the 4-color scheme; red is bright enough as the focus color). A leading
>prefix is also drawn for redundancy. - Non-selected items: white on black.
- Disabled items (e.g.,
Viewwhen no mnemonic is loaded): dim gray on black. - Footers / hint text: dim gray on black.
1. Goal
Replace the current "generate-random-mnemonic-on-boot, immediately go to working screen" proof of concept with a navigable on-device UI that lets the operator:
- See a splash screen at power-on.
- Choose at the main menu between Generate a fresh mnemonic, Enter an existing mnemonic, or View the currently loaded mnemonic.
- After Generate or successful Enter, transition into the existing Working Page (sign / approve flow that already exists today).
- Persistence is out of scope for this plan — every boot starts at the splash, mnemonic lives only in RAM. A follow-up plan will add PIN-protected NVS storage.
2. Hardware / display constraints
From the existing firmware (display.c):
- Panel: 240×135, ST7789, landscape.
- Font cell at
CHAR_SCALE = 2:CHAR_W = 12,CHAR_H = 16. - Usable text grid: 20 columns × 8 rows (with ~0px margin) or 19 columns × 6 rows with the comfortable insets the user noted. Plan assumes 19×6 so we have ≥4px breathing room on every edge.
- 3 buttons (current physical mapping in
main.c):D0= GPIO0, active LOW (boot button, internal pull-up)D1= GPIO1, active HIGH (external pull-down)D2= GPIO2, active HIGH (external pull-down)
2.1 New menu-button semantics
| Button | Role in new UI | Existing role (working page) |
|---|---|---|
D2 |
Up (scroll up) | "always allow" approval |
D1 |
Down (scroll down) | "approve once" |
D0 |
Enter / select | "deny" |
The working-page button semantics intentionally stay the same as today (D0=deny, D1=approve once, D2=always). Only menu screens reinterpret them as Up/Down/Enter. The button-handling code is split into two layers (see §5.2) so the working-page approval flow does not have to change behavior.
3. Screen flow
flowchart TD
A[Power on] --> B[Splash: N_SIGNER]
B -- D0 or D1 or D2 --> C[Main Menu]
C -- Generate selected --> D[Generate page: random 12 words]
D -- auto --> E[View page]
C -- Enter selected --> F[Enter page: pick letter]
F -- letter chosen --> G[Pick word starting with letter]
G -- word chosen --> H[Phrase preview: continue or delete]
H -- continue and len 12 --> I[Save prompt]
H -- continue and len lt 12 --> F
H -- delete --> H2[Drop last word]
H2 --> H
I -- save --> W[Working page]
I -- back --> C
C -- View selected --> V[View page read-only]
V -- any button --> W
E -- any button --> W
W -. menu hotkey not in scope .-> C
Notes:
- From the Working page there is currently no "back to menu" hotkey. That is out of scope for this plan; once entered, the working page is the run-mode terminus until reboot. Adding a long-press-to-menu is left as a tracked TODO at the end of this doc.
- The View page is reachable from two places: post-Generate (auto), and from the main menu (read-only). Both behave the same — "any button returns to caller". Caller is tracked so that View-from-Generate goes to Working, and View-from-Menu returns to Menu.
4. Per-screen specification
All screens use display_clear(RGB565_BLACK) as the base, then draw text with the existing 12×16 cell. Coordinates below are pixel offsets from top-left.
4.1 Splash screen
- Centered horizontally and vertically: the literal string
N_SIGNER, colorRGB565_RED, on black background. - 8 chars × 12px = 96px wide → x = (240−96)/2 = 72.
- 1 row × 16px → y = (135−16)/2 ≈ 60.
- Below the title (y = 60 + 24 = 84), a hint line
"press any button"inRGB565_DIM_GRAY, centered (small font). - Wait for any of the three buttons (debounced, see §5.1). The "drain held buttons first, then read fresh edge" pattern from
wait_for_approval()is reused. - No timeout — strictly button-only advance, per user direction.
4.2 Main menu
- Title row at y=4:
"MAIN MENU"inRGB565_WHITE(centered). - Three menu items at y=28, y=52, y=76:
GenerateEnterView
- The selected item is rendered as red text on black with a leading
>prefix. Non-selected items are white on black with no prefix (a 2-char gap so positions don't shift). Disabled items are dim gray. - Footer at y=120:
"D2 up D1 dn D0 ok"inRGB565_DIM_GRAY(small font). - Behavior:
D2(Up): selection moves up, wrapping from Generate → View at the top edge.D1(Down): selection moves down, wrapping from View → Generate at the bottom edge.D0(Enter): commit the selected item.
- View is drawn in
RGB565_DIM_GRAYand not selectable when no mnemonic is loaded. The cursor skips it on Up/Down navigation in that state.
4.3 Generate page
- Renders the title
"GENERATING..."centered inRGB565_WHITEwhilegenerate_mnemonic_12()is called. - On success: store the resulting mnemonic in
s_mnemonic(already exists inmain.c), then transition to the View page withcaller=GENERATE. - On failure: show
"GEN FAILED"inRGB565_REDfor ~2 seconds, then return to main menu. - This page has no button input loop of its own — it's a transient pass-through.
4.4 View page
- Title row at y=2:
"YOUR MNEMONIC"inRGB565_WHITE(small font), centered. Only shown if there is enough vertical room — see layout below. - 12 words laid out as 3 columns × 4 rows.
- Column widths: each column gets ⌊240/3⌋ = 80px.
- Each cell shows
"NN word"whereNNis 1-based index zero-padded to 2 digits andwordis the BIP-39 word. - Longest BIP-39 word is 8 chars, plus
"NN "prefix = 11 chars total. 11 × 12 = 132px — that does NOT fit in an 80px column. - Resolution: drop the index prefix from the cell and rely on row-major order (top-left = #1, top-right = #3, bottom-left = #10, bottom-right = #12). Give a tiny index legend in the footer ("L→R, top→bottom") instead. 8 chars × 12 = 96px still > 80px column.
- Real resolution: drop
CHAR_SCALEto 1 (6×8 cells) for the View page only. That gives 40 cols × 16 rows. 12 words in 3×4 grid then becomes trivial: each cell is up to 13 chars ("NN. word ") at 6px = 78px, fits in the 80px column with margin. We add adisplay_draw_text_small()variant (see §6.1) that usesCHAR_SCALE=1. - Final layout (small font):
- Title at y=2, height 8.
- Grid starts at y=14. Row pitch = 24px (8px cell + 16px gap for readability). Last row at y=14+3·24 = 86.
- Footer at y=110:
"any key = back"inRGB565_DIM_GRAY.
- Cell text format:
"%2d. %s"(e.g.," 1. abandon"), colorRGB565_WHITEon black.
- Behavior: any button (debounced) returns to the caller (
GENERATE→ working page;MENU→ main menu). - The working-page button semantics are not active here.
4.5 Enter page (multi-stage)
This page is the most complex. It has three sub-states implemented in a sub-state machine:
4.5.1 Stage A — Letter picker
- Top line:
"WORD %d/12"(e.g.,"WORD 3/12") inRGB565_WHITE. - Second line:
"PICK FIRST LETTER"inRGB565_DIM_GRAY(small font). - A 26-letter grid of the alphabet:
- With small font (6×8): a row of 26 chars = 156px, fits in one row. But we want a visible cursor on the selected letter, so use 2 rows of 13 letters each (
A..M/N..Z) at large font (12×16 with extra spacing) — 13 × 14px = 182px wide, fits. - At y=40 row 1 (
A B C D E F G H I J K L M). - At y=64 row 2 (
N O P Q R S T U V W X Y Z). - Non-selected letters:
RGB565_WHITEon black. Selected letter:RGB565_REDon black (no fill — pure color cue).
- With small font (6×8): a row of 26 chars = 156px, fits in one row. But we want a visible cursor on the selected letter, so use 2 rows of 13 letters each (
- Footer at y=120:
"D2 up D1 dn D0 ok"inRGB565_DIM_GRAY(small font). - Behavior:
D2: move cursor up — A↔N, B↔O, …, M↔Z (wrap top-to-bottom within column).D1: move cursor right (and wrap onto next row); fromZwrap toA.- Note: with only 3 buttons we cannot have separate Left/Right/Up/Down. The Up/Down semantics here mean "move within the alphabet" —
D2= previous letter,D1= next letter, with wrap. The visual 2-row layout is purely cosmetic (so the operator can see the whole alphabet at a glance); navigation is linear, not 2D. The selected letter is highlighted wherever it lands in the 2-row display. D0: commit selected letter, move to Stage B.
- A back mechanic is needed too. Convention: holding
D0for >1 second cancels Stage A and returns to the main menu (only meaningful if no words have been entered yet); otherwise it returns to Stage C of the previous word. See §4.5.4.
4.5.2 Stage B — Word picker
- Top line:
"WORD %d/12 [%c]"showing the chosen letter (e.g.,"WORD 3/12 [P]") inRGB565_WHITE. - Below: the list of all BIP-39 words starting with that letter, scrollable. Word counts per letter range from 1 (e.g.,
x) up to ~150 (most common letters). Use small font (6×8). - Layout with small font: 16 visible word rows at row pitch 8+1=9px starting at y=14, last row at y=14+15·9=149 — overflows. Cap visible rows at 12, row pitch 9px, y=14..14+11·9=113.
- 12 rows × 1 column is fine; longest word ≤ 8 chars × 6px = 48px, leaves plenty of room. We can also do 2 columns of 12 if needed for letters like
s(~250 words won't happen — BIP-39 max per letter is around 91 fors). 1 column × 12 visible with scroll keeps it simple.
- 12 rows × 1 column is fine; longest word ≤ 8 chars × 6px = 48px, leaves plenty of room. We can also do 2 columns of 12 if needed for letters like
- Cursor: highlighted row drawn in
RGB565_REDon black (with a leading>prefix for redundancy). Non-cursor rows inRGB565_WHITEon black. - Footer at y=120:
"D2 up D1 dn D0 ok"inRGB565_DIM_GRAY(small font). - Behavior:
D2: prev word (with scroll up at top of viewport, wrap from first to last word in that letter group).D1: next word (with scroll down at bottom of viewport, wrap last → first).D0: commit selected word, append to the in-progress mnemonic buffer, move to Stage C.
The full BIP-39 wordlist from mnemonic_wordlist.h is already linked in. We add a small helper that produces (start_index, count) pairs for each letter on first use and caches them in a static array of 26 (uint16_t start, uint16_t count). See §6.2.
4.5.3 Stage C — Phrase preview / continue / delete
- Top line:
"WORDS SO FAR (%d/12)"inRGB565_WHITE. - Body: the entered words rendered in a 3-column grid using small font, same as the View page (§4.4). Empty slots are blank. The most recently added word is drawn in
RGB565_RED(visual confirmation of the just-committed entry); previously confirmed words inRGB565_WHITE. - Footer (two lines) at y=104, y=120, small font:
- y=104:
"D2 continue D1 delete"inRGB565_DIM_GRAY. - y=120:
"D0 save"inRGB565_WHITEwhencount == 12, inRGB565_DIM_GRAYotherwise.
- y=104:
- Behavior:
D2(continue): ifcount < 12, go back to Stage A for wordcount+1. Ifcount == 12, no-op.D1(delete): drop the last word from the buffer, redraw. Ifcountwas 1, drop to 0. Ifcountbecomes 0, this stays on Stage C with an empty grid; pressing continue again restarts at word 1.D0(save): only valid ifcount == 12. Otherwise no-op. On save, validate the full mnemonic via the BIP-39 checksum check (mnemonic_validate(), see §6.3). On valid: copy buffer intos_mnemonic, transition to working page. On invalid: show"BAD CHECKSUM"inRGB565_REDfor 2s and return to Stage C with all 12 words still present (so user can delete and try again).
4.5.4 Cancel / back from Enter
- Long-press
D0(>1 second) at any stage of Enter cancels and returns to main menu without saving; the in-progress buffer is wiped viasecure_memzero-equivalent (we will addsecure_memzerofor the firmware — see §6.4). - Short-press
D0is the normal "select / save" semantic.
4.6 Working page (existing, retro-fitted to 4-color palette)
- This is the existing approval-loop screen. Behavior change in this plan: we arrive here from the menu/save flow rather than from boot directly. Button semantics remain unchanged (D0=deny, D1=once, D2=always).
- Existing
show_idle_screen()andshow_approval_prompt_with_caller()currently use yellow/cyan/green/magenta. They are retro-fitted to the 4-color palette:- All informational labels →
RGB565_WHITE. - The "APPROVE sign_event?" header →
RGB565_RED(it's a focus/attention call-out). - All hints/footers ("D1=allow once" etc.) →
RGB565_DIM_GRAY. - Status counters at the bottom of the running screen →
RGB565_DIM_GRAY.
- All informational labels →
- The existing key derivation (mnemonic → seed → privkey → pubkey → npub) is moved out of
app_main()into a functionapply_mnemonic_and_enter_working()that is called from bothGenerate→View→WorkingandEnter→Save→Workingpaths. See §5.4.
5. Code structure changes
5.1 Button input layer
Add a small input module: firmware/feather_s3_tft/main/buttons.h, firmware/feather_s3_tft/main/buttons.c.
Public API:
typedef enum {
BTN_NONE = 0,
BTN_D0, /* aka ENTER in menu mode, DENY in working mode */
BTN_D1, /* aka DOWN in menu mode, APPROVE_ONCE in working mode */
BTN_D2, /* aka UP in menu mode, APPROVE_ALWAYS in working mode */
} btn_id_t;
typedef enum {
BTN_EVT_NONE = 0,
BTN_EVT_PRESS, /* short press (release before LONG_MS) */
BTN_EVT_LONG_PRESS, /* held for >= LONG_MS, fires on release */
} btn_event_kind_t;
typedef struct {
btn_id_t id;
btn_event_kind_t kind;
} btn_event_t;
void buttons_init(void);
/* Block until a press/long-press event arrives, or timeout_ms expires.
* Returns BTN_EVT_NONE on timeout. timeout_ms == 0xFFFFFFFFu = wait forever. */
btn_event_t buttons_wait(uint32_t timeout_ms);
/* Non-blocking: returns BTN_EVT_NONE if no event ready. */
btn_event_t buttons_poll(void);
The existing wait_for_approval() in main.c is rewritten as a thin wrapper over buttons_wait() so the existing approval semantics still work without code duplication.
Constants:
BTN_DEBOUNCE_MS = 30(existing)BTN_LONG_MS = 1000BTN_POLL_MS = 10
5.2 UI state machine
Add firmware/feather_s3_tft/main/ui.h, firmware/feather_s3_tft/main/ui.c.
typedef enum {
UI_SPLASH,
UI_MAIN_MENU,
UI_GENERATE,
UI_VIEW,
UI_ENTER_LETTER,
UI_ENTER_WORD,
UI_ENTER_PREVIEW,
UI_WORKING, /* delegates to existing main loop */
} ui_screen_t;
typedef enum { VIEW_FROM_GENERATE, VIEW_FROM_MENU } view_caller_t;
typedef struct {
char words[12][12]; /* room for 8-char word + nul, padded for safety */
uint8_t count; /* 0..12 */
int8_t letter_idx; /* 0..25, current cursor in Stage A */
uint16_t word_cursor; /* cursor index inside current letter group */
uint16_t word_scroll; /* top of viewport in word list */
view_caller_t view_caller;
} ui_state_t;
/* Run the UI until a mnemonic is loaded and the user requests transition to working. */
void ui_run_until_working(char out_mnemonic[256]);
ui_run_until_working() is called from app_main() after display init and before the main USB/sign loop. It returns when the user has either:
- generated a mnemonic and dismissed the View screen, or
- entered a valid 12-word mnemonic and pressed Save.
The mnemonic is written into out_mnemonic. Caller is responsible for zeroing the local UI buffer (this is done inside ui.c before return, but main.c also keeps its own copy ephemeral).
Each screen is a function ui_screen_splash(), ui_screen_main_menu(), etc. Each one returns the next ui_screen_t enum, plus mutates ui_state_t as needed. The dispatcher is a simple while (current != UI_WORKING) { current = handlers[current](&state); }.
5.3 Display additions
Add to display.h:
/* CHAR_SCALE=1 variant (6x8 cells, 40 cols x 16 rows). */
esp_err_t display_draw_text_small(int x, int y, const char *text, uint16_t fg, uint16_t bg);
/* Convenience: centered text using the regular (12x16) font. */
esp_err_t display_draw_text_centered(int y, const char *text, uint16_t fg, uint16_t bg);
esp_err_t display_draw_text_small_centered(int y, const char *text, uint16_t fg, uint16_t bg);
/* Inverse-video helper used by selected menu items.
* Draws a filled rect of the bg color sized to the text, then text on top. */
esp_err_t display_draw_text_inverse(int x, int y, const char *text, uint16_t bg, uint16_t fg);
Implementation notes:
- The existing
display_draw_text()(line approximate, locate via search) hardcodesCHAR_SCALE = 2. Refactor it so the scale is parameterized in an internal helper, thendisplay_draw_textanddisplay_draw_text_smallare thin wrappers. - Centered helpers compute
x = (DISPLAY_WIDTH - char_w * strlen) / 2and call the underlying drawer.
Add a dim color constant for grayed-out menu items:
#define RGB565_DIM_GRAY 0x4208 /* ~25% white */
5.4 main.c refactor
Sketch of the new app_main():
void app_main(void) {
display_init();
buttons_init();
/* draw splash, main menu, etc. */
ui_run_until_working(s_mnemonic);
/* now s_mnemonic holds 12 words, derive keys */
if (apply_mnemonic_and_enter_working(s_mnemonic) != 0) {
/* show error, halt; future plan adds "back to menu" */
}
/* zero s_mnemonic immediately after key derivation, same as today */
secure_memzero(s_mnemonic, sizeof(s_mnemonic));
transport_init();
/* existing USB/sign loop */
run_signing_loop();
}
Where apply_mnemonic_and_enter_working() does what lines 800–836 of main.c currently do (mnemonic→seed→privkey→pubkey→npub→idle screen draw), refactored into one function.
The big while (1) USB loop body (currently lines 852–1004) is moved into run_signing_loop() for readability. No behavior change.
The existing global statics (s_pubkey_hex, s_privkey, s_npub, s_npub_short, s_mnemonic_short, s_frame_buf, s_response_buf, s_signed_event_buf) keep their current scope.
6. Helpers / library additions
6.1 Small font drawing
Already covered in §5.3. The existing font5x7.h is 5×7 inside a 6×8 cell, so display_draw_text_small() literally just drops the CHAR_SCALE multiplier. The existing scale-2 path is preserved.
6.2 BIP-39 letter index
Add to mnemonic.h:
/* Returns the number of BIP-39 words starting with `letter` (a-z, case insensitive),
* and writes the start index in the global wordlist into *out_start. */
size_t mnemonic_words_for_letter(char letter, size_t *out_start);
/* Returns pointer to the i-th BIP-39 word (0..2047) in the global list. */
const char *mnemonic_word_at(size_t index);
Implementation: iterate the wordlist once on first call, cache 26 (start, count) pairs in a static array.
6.3 Mnemonic checksum validation
The current mnemonic.c provides generate_mnemonic_12 and mnemonic_to_seed. For Enter mode we need to validate user-typed input independently of seed derivation (so we can show a checksum error before committing keys).
Add to mnemonic.h:
/* Validate a space-separated 12-word mnemonic against BIP-39 checksum.
* Returns 0 on valid, negative on word-not-in-list / wrong-count / bad-checksum. */
int mnemonic_validate(const char *mnemonic);
If mnemonic_to_seed() already returns nonzero on bad checksum, mnemonic_validate() can be a thin wrapper that runs the checksum half but skips PBKDF2. Confirm this during implementation by reading mnemonic.c — if the existing function only does PBKDF2 and trusts the input, we add the proper BIP-39 checksum check (entropy bytes from word indices + last 4 bits = SHA256(entropy)[0] >> 4).
6.4 Secure memzero
Add a tiny secure_memzero() to a new firmware/feather_s3_tft/main/secure_mem.h using volatile writes, modeled on the existing src/secure_mem.c. Not strictly new functionality (we already do memset(buf,0,sizeof) in many places) but the in-progress mnemonic buffer in ui_state_t and word stage buffers should use it for clarity.
7. CMakeLists updates
firmware/feather_s3_tft/main/CMakeLists.txt adds the new sources:
buttons.cui.csecure_mem.c
SRCS list grows by these three files. No new component dependencies.
8. Out of scope (tracked TODOs)
These are intentionally deferred and listed here so they don't get lost:
- Persistence (PIN-protected NVS storage of the seed). Will be a separate plan,
plans/feather_signer_persistence.md— to be authored after this UI plan lands. It will reuse the same letter/digit scroll UI for PIN entry. - "Back to menu" hotkey from the working page. Today, once you arrive at working, you're stuck until reboot. A long-press gesture (e.g., D0+D2 simultaneously, or D0 long-press during idle screen only) could re-enter the menu and lock the loaded mnemonic.
- Lock screen / re-auth. Without PIN, the device is fully unlocked once a mnemonic is entered. The persistence plan will add a "lock" hotkey that wipes RAM keys and returns to the splash/PIN-entry screen.
- Display blanking / screensaver. ST7789 doesn't burn-in dramatically but reducing the backlight after idle would be polite.
- Test plan additions. The current firmware has no automated tests on-device; we rely on host-side tests in
tests/. UI logic inui.cshould be written so that the screen-transition functionui_next_screen(state, event)is pure (no display/button calls inside) and can be unit-tested with a host-side harness. This is a soft target — recommend doing it but not blocking on it.
9. Acceptance criteria
- Power-on shows the red
N_SIGNERsplash; pressing any of D0/D1/D2 advances to the main menu. - Main menu lists
Generate,Enter,View.Viewis dimmed and unselectable when no mnemonic is loaded. - In the main menu, D2 moves selection up with wrap, D1 moves down with wrap, D0 commits. Selected row is shown in inverse video.
- Selecting
Generateproduces a fresh random 12-word BIP-39 mnemonic, displays it on the View page in a 3×4 grid, and any button press transitions into the working page (key derivation + USB/sign loop). - Selecting
View(after a mnemonic is loaded) shows the same View page, and any button press returns to main menu. - Selecting
Enter:- Stage A presents the alphabet with a movable highlight; D2/D1 navigate, D0 picks.
- Stage B presents all BIP-39 words for the chosen letter, scrollable; D2/D1 navigate, D0 picks.
- Stage C shows the running phrase preview as a 3×4 grid; D2 continues to next word, D1 deletes last word, D0 saves (only when 12 words are present).
- Long-press D0 cancels and returns to main menu, wiping the partial buffer.
- Save with valid checksum transitions to working page; save with invalid checksum shows
"BAD CHECKSUM"for 2s and stays on Stage C.
- Working-page button semantics (D0=deny, D1=once, D2=always) remain identical to today.
- No mnemonic persistence: a reboot starts at the splash with no loaded mnemonic.
- All in-progress buffers (
ui_state_t.words[][], the workings_mnemonic) are wiped viasecure_memzeroon transition out of the screens that use them.
10. Mermaid: button semantics by screen
flowchart LR
subgraph Splash
Sp[D0 D1 D2 advance]
end
subgraph MainMenu
M2[D2 up] --- M1[D1 down] --- M0[D0 enter]
end
subgraph EnterLetter
EL2[D2 prev letter] --- EL1[D1 next letter] --- EL0[D0 select / long=cancel]
end
subgraph EnterWord
EW2[D2 prev word] --- EW1[D1 next word] --- EW0[D0 select / long=cancel]
end
subgraph EnterPreview
EP2[D2 continue] --- EP1[D1 delete last] --- EP0[D0 save / long=cancel]
end
subgraph View
V[any button = back]
end
subgraph Working
W2[D2 always allow] --- W1[D1 approve once] --- W0[D0 deny]
end