Show a confirm dialog and act on the answer
Dialogs.confirm returns an Overlay value — it does not take
callbacks. The app owns the dialog's open/closed state and the
focused button; key events route to whichever side owns focus.
Pattern
- Add a flag (or sealed
case class) to your model that says "the confirm dialog is open and waiting for an answer". - In
view, when that flag is set, append theDialogs.confirmoverlay toRootNode.overlays. - In
update, route key events to the dialog when it's open: arrow keys flip the focused button,Enter/Spacecommits,Escapecancels.
Code
import termflow.tui.{Dialogs, Theme}
import termflow.tui.KeyDecoder.InputKey.*
final case class Model(
/* … domain fields … */,
confirm: Option[ConfirmState]
)
enum ConfirmState:
case Open(yesFocused: Boolean) // dialog visible, button focus tracked here
enum Msg:
case AskDelete // user requested the destructive action
case ConfirmYes
case ConfirmNo
case ConfirmKey(k: InputKey)
def update(m: Model, msg: Msg, ctx: RuntimeCtx[Msg]): Tui[Model, Msg] =
msg match
case Msg.AskDelete =>
m.copy(confirm = Some(ConfirmState.Open(yesFocused = false))).tui
case Msg.ConfirmKey(k) =>
m.confirm match
case Some(ConfirmState.Open(yesFocused)) =>
k match
case ArrowLeft | ArrowRight | Tab | BackTab =>
m.copy(confirm = Some(ConfirmState.Open(!yesFocused))).tui
case Enter | CharKey(' ') =>
if yesFocused then m.copy(confirm = None).gCmd(Msg.ConfirmYes)
else m.copy(confirm = None).gCmd(Msg.ConfirmNo)
case Escape =>
m.copy(confirm = None).gCmd(Msg.ConfirmNo)
case _ => m.tui
case None => m.tui
case Msg.ConfirmYes => /* perform the destructive action */
case Msg.ConfirmNo => m.tui /* no-op */
def view(m: Model): RootNode =
given Theme = Theme.dark
val baseChildren = /* … */
val overlays = m.confirm match
case Some(ConfirmState.Open(yesFocused)) =>
List(Dialogs.confirm(
prompt = "Delete this file?",
yesFocused = yesFocused,
title = "Confirm delete",
yesLabel = "Yes",
noLabel = "No"
))
case None => Nil
RootNode(80, 24, children = baseChildren, input = None, overlays = overlays)
Notes
- Why
Option[ConfirmState]and not justBoolean? It scales — addOpen(yesFocused, prompt: String)later and any caller ofAskDeletecan pass its own prompt. - Always re-route keys when a dialog is open. Don't forget to
intercept the global keymap so
qdoesn't quit while the dialog is modal. The simplest pattern is: ifm.confirm.isDefined, route the whole keystream intoConfirmKeyand skip your normal handlers. - Mouse clicks on overlays.
Dialogs.confirmis mouse-aware in the renderer; if you want clicks on the buttons to flip focus, pair the dialog withLayout.resolveTrackedand dispatchConfirmKeyfrom the hit-test result.
For a working example, see the showcase's Dialogs tab —
Stage1ShowcaseApp.scala.