QuickUI Dialog User Guide

May 2, 2026 · View on GitHub

quickui#dialog#open() provides a data-driven dialog system. Simply declare a list of controls, and it pops up a dialog containing inputs, radio buttons, checkboxes, dropdowns, buttons, separators, etc. Once the user finishes interacting, all control values are returned.

Quick Start

let items = [
    \ {'type': 'label', 'text': 'Please fill in:'},
    \ {'type': 'input', 'name': 'username', 'prompt': 'Name:', 'value': 'skywind'},
    \ {'type': 'input', 'name': 'email', 'prompt': 'Email:'},
    \ {'type': 'button', 'name': 'confirm', 'items': [' &OK ', ' &Cancel ']},
    \ ]

let result = quickui#dialog#open(items, {'title': 'User Info'})

if result.button ==# 'confirm' && result.button_index == 0
    echo 'Name: ' . result.username
    echo 'Email: ' . result.email
endif

Result:

┌─ User Info ──────────────────────────────X┐
│                                           │
│  Please fill in:                          │
│                                           │
│  Name:  [skywind                       ]  │
│  Email: [                              ]  │
│                                           │
│            < OK >    < Cancel >           │
│                                           │
└───────────────────────────────────────────┘

API

let result = quickui#dialog#open(items [, opts])
  • itemsList<Dict>, each element describes a control
  • optsDict (optional), dialog-level options
  • Returns — Dict, containing all control values and exit status

Control Types

label — Static Text

Not focusable. Used to display descriptive text.

{'type': 'label', 'text': 'Please fill in the form:'}
{'type': 'label', 'text': ['Line 1', 'Line 2']}   " multiline
FieldTypeRequiredDescription
typeStringYes'label'
textString / ListYesDisplay text. String is split by \n; List uses one element per line

input — Single-line Text Input

Focusable. Built-in readline editing (cursor movement, selection, clipboard, history browsing).

{'type': 'input', 'name': 'username', 'prompt': 'Name:', 'value': 'skywind'}
FieldTypeRequiredDefaultDescription
typeStringYes'input'
nameStringYesControl name, used as key in return value
promptStringNo''Label text on the left side
valueStringNo''Initial text
historyStringNo''History namespace (shared across calls)
focusNumberNoIf present, this control receives initial focus (value ignored)

Editing keybindings (when input is focused):

KeyAction
Regular charactersInsert
Left / RightMove cursor
Home / EndBeginning / end of line
Ctrl+A / Ctrl+EBeginning / end of line
Backspace / DeleteDelete character
Ctrl+K / Ctrl+UDelete to end / beginning of line
Ctrl+WDelete previous word
Shift+Left/RightSelect text
Ctrl+C / Ctrl+VCopy / paste
Ctrl+Up / Ctrl+DownBrowse history
EnterConfirm dialog
Up / DownMove focus to previous / next control
Tab / S-TabMove focus forward / backward

radio — Radio Button Group

Focusable. Use Left/Right to move the visual cursor between options, Space to confirm selection ((*)). The (*) marker only moves on Space; arrow keys move the highlight only.

{'type': 'radio', 'name': 'role', 'prompt': 'Role:',
 \ 'items': ['&Dev', '&QA', '&PM'], 'value': 0}
FieldTypeRequiredDefaultDescription
typeStringYes'radio'
nameStringYesControl name
promptStringNo''Label on the left side
itemsListYesOption text list; & marks the hotkey character
valueNumberNo0Default selected index (0-based)
verticalNumberNoauto0 forces horizontal, 1 forces vertical; auto if omitted
focusNumberNoIf present, this control receives initial focus; value sets cursor position (0-based item index)

Horizontal layout: Role: (*) Dev ( ) QA ( ) PM

Vertical layout (auto-switches when options are too wide):

Role:  (*) Development
       ( ) Quality Assurance
       ( ) Project Management
KeyAction
Left / hMove cursor to previous option (cycles)
Right / lMove cursor to next option (cycles)
SpaceSelect the option under cursor (moves (*) marker)
EnterConfirm dialog
Up / DownMove focus (horizontal mode), or navigate within items with boundary overflow to adjacent control (vertical mode)

check — Checkbox

Focusable. Space toggles the checked state.

{'type': 'check', 'name': 'admin', 'text': '&Administrator'}
{'type': 'check', 'name': 'notify', 'text': 'Send &notification', 'value': 1}
FieldTypeRequiredDefaultDescription
typeStringYes'check'
nameStringYesControl name
textStringYesDisplay text; & marks the hotkey character
promptStringNo''Label on the left side (participates in prompt alignment when set)
valueNumberNo00 = unchecked, 1 = checked
focusNumberNoIf present, this control receives initial focus (value ignored)

Layout: [x] Administrator or Admin: [x] Administrator (with prompt)

KeyAction
SpaceToggle check
EnterConfirm dialog
Up / DownMove focus

button — Button Row

Focusable. Buttons are centered. Activating any button closes the dialog.

{'type': 'button', 'name': 'confirm', 'items': [' &OK ', ' &Cancel ']}
FieldTypeRequiredDefaultDescription
typeStringYes'button'
nameStringNo'button'Control name
itemsListYesButton text list; & marks the hotkey character
valueNumberNo0Default focused button index (0-based)
focusNumberNoIf present, this control receives initial focus; value sets which button is highlighted (0-based)

Layout: < OK > < Cancel >

KeyAction
Left / hSwitch to left button
Right / lSwitch to right button
Space / EnterActivate current button and close dialog

separator — Separator Line

Not focusable. Draws a horizontal line to visually group controls.

{'type': 'separator'}
FieldTypeRequiredDescription
typeStringYes'separator'

Layout (using border character):

├───────────────────────────────────────────┤

The separator character matches the dialog border style (e.g., for single-line borders, - for ASCII borders). When the dialog has no border, a plain - is used.

Note: Separators replace the automatic gap between controls — no blank lines are inserted before or after a separator.

Focusable. Displays a collapsed selection field that opens a self-drawn popup list when activated.

{'type': 'dropdown', 'name': 'lang', 'prompt': 'Language:',
 \ 'items': ['Python', 'C/C++', 'Java', 'Go'], 'value': 1}
FieldTypeRequiredDefaultDescription
typeStringYes'dropdown'
nameStringYesControl name, used as key in return value
promptStringNo''Label text on the left side (participates in prompt alignment)
itemsListYesOption text list
valueNumberNo0Default selected index (0-based); clamped to valid range
focusNumberNoIf present, this control receives initial focus (value ignored)

Collapsed layout:

Language:  [C/C++                        v]

When activated (Enter/Space/click), a popup list appears below the control:

Language:  [C/C++                        v]
           ┌─────────────────────────────┐
           │  Python                     │
           │> C/C++                      │
           │  Java                       │
           │  Go                         │
           └─────────────────────────────┘

Collapsed keybindings (when dropdown is focused in dialog):

KeyAction
Enter / SpaceOpen popup list
Left / hSelect previous item (wraps)
Right / lSelect next item (wraps)
Up / DownMove focus to adjacent control

Popup keybindings (when popup list is open):

KeyAction
Up / kMove highlight up
Down / jMove highlight down
Home / ggJump to first item
End / GJump to last item
PageUp / PageDownScroll by page
Enter / SpaceConfirm selection and close popup
EscCancel and close popup (value unchanged)
Mouse clickSelect clicked item and close popup

Dialog Options

Passed via the second parameter opts:

OptionTypeDefaultDescription
titleString'Dialog'Title text
wNumberautoContent area width (auto-calculated if omitted)
min_wNumber40Minimum width for auto-calculation
borderNumberg:quickui#style#borderBorder style
centerNumber1Whether to center the dialog
paddingList[1,1,1,1]Inner padding [top, right, bottom, left]
colorString'QuickBG'Background highlight group
bordercolorString'QuickBorder'Border highlight group
gapNumber1Number of blank lines between different control types
buttonNumber1Whether to show the close button
validatorFuncrefValidation function called before normal exit (see below)

Validator

If opts.validator is provided, it is called before the dialog exits normally (i.e., button_index >= 0). It is not called on cancel (ESC / Ctrl-C / close button).

The function receives a single argument — the same Dict that open() would return — and should return:

  • 0 or '' (empty string) — validation passed, dialog exits normally
  • A non-empty string — validation failed, the string is displayed as an error message with ErrorMsg highlight, and the dialog remains open
function! MyValidator(result) abort
    if a:result.username ==# ''
        return 'Username cannot be empty!'
    endif
    return ''
endfunc

let result = quickui#dialog#open(items, {
    \ 'title': 'Form',
    \ 'validator': function('MyValidator'),
    \ })

Return Value

Returns a Dict. All control values are always included, regardless of confirm or cancel:

FieldTypeDescription
buttonStringName of the button that triggered exit; '' for Enter confirm or cancel
button_indexNumberButton index (0-based); 0 for Enter confirm or first button; -1 for cancel
<input.name>StringText content of the input
<radio.name>NumberSelected option index (0-based)
<check.name>NumberChecked state (0/1)
<dropdown.name>NumberSelected item index (0-based)

Detecting Exit Method

let r = quickui#dialog#open(items, opts)

" User clicked the OK button (button name='confirm', OK is the 1st button)
if r.button ==# 'confirm' && r.button_index == 0
    " Handle confirm logic
endif

" User pressed Enter from input/radio/check (button='' but button_index=0)
" NOTE: distinguish from button click by checking r.button ==# ''
if r.button ==# '' && r.button_index == 0
    " Handle Enter confirm
endif

" User cancelled (ESC / Ctrl-C / close button: button='' and button_index=-1)
if r.button ==# '' && r.button_index == -1
    " Handle cancel (r still contains user-modified values)
endif

Hotkeys

Use & in button, radio, and check text to mark a hotkey character (e.g., ' &OK ' makes O the hotkey).

  • Button hotkey — directly activates the button and closes the dialog
  • Radio hotkey — selects the corresponding option without closing
  • Check hotkey — toggles the checkbox without closing

Hotkeys are globally active when focus is not on an input. When an input is focused, all characters are treated as text input and hotkeys are disabled.

Focus Navigation

KeyAction
TabMove focus to next control (wraps around)
Shift-TabMove focus to previous control (wraps around)
UpMove focus backward (vertical intuition); in vertical radio, moves cursor within items first
DownMove focus forward (vertical intuition); in vertical radio, moves cursor within items first

Initial focus defaults to the first focusable control. Use the control-level focus field to specify initial focus:

let items = [
    \ {'type': 'input', 'name': 'email', 'prompt': 'Email:'},
    \ {'type': 'button', 'items': [' &OK ', ' &Cancel '], 'focus': 1},
    \ ]
let result = quickui#dialog#open(items)

The focus field is an integer. For button/radio, it specifies the sub-item index (which button or radio item gets cursor). For other controls, the value is ignored — just having the field means "start focus here".

Layout Rules

Vertical Stacking

Controls are arranged top-to-bottom in the order of items.

Blank Line Separation

  • Adjacent controls of different types are separated by gap blank lines (default: 1)
  • Adjacent controls of the same type have no blank lines, forming a visual group
  • Separators replace gaps — no blank lines are inserted before or after a separator

Prompt Alignment

Consecutive controls with prompts (input, radio, dropdown, check with prompt) are automatically aligned:

Name:      [skywind                       ]
Email:     [                              ]
Language:  [Python                       v]
Role:      (*) Dev  ( ) QA  ( ) PM

Labels and separators do not break alignment groups; only interactive controls without a prompt break the group.

Mouse Support

  • Click input — focus and position cursor
  • Click radio option — focus and select that option
  • Click check — focus and toggle check state
  • Click dropdown — focus and open popup list
  • Click button — activate that button and close dialog
  • Click close button (X) — cancel

Complete Examples

User Form

let items = [
    \ {'type': 'label', 'text': 'Please fill in the user form:'},
    \ {'type': 'input', 'name': 'username', 'prompt': 'Name:',
    \  'value': 'skywind'},
    \ {'type': 'input', 'name': 'email', 'prompt': 'Email:'},
    \ {'type': 'radio', 'name': 'role', 'prompt': 'Role:',
    \  'items': ['&Dev', '&QA', '&PM'], 'value': 0},
    \ {'type': 'check', 'name': 'admin', 'text': '&Administrator'},
    \ {'type': 'check', 'name': 'notify', 'text': 'Send &notification',
    \  'value': 1},
    \ {'type': 'button', 'name': 'confirm',
    \  'items': [' &OK ', ' &Cancel ']},
    \ ]

let result = quickui#dialog#open(items, {
    \ 'title': 'User Form', 'w': 50})

if result.button ==# 'confirm' && result.button_index == 0
    echo 'User: ' . result.username
    echo 'Email: ' . result.email
    echo 'Role: ' . result.role
    echo 'Admin: ' . result.admin
    echo 'Notify: ' . result.notify
endif

Simple Confirmation Dialog

let items = [
    \ {'type': 'label', 'text': 'Are you sure you want to delete this file?'},
    \ {'type': 'button', 'name': 'confirm',
    \  'items': [' &Yes ', ' &No ']},
    \ ]

let result = quickui#dialog#open(items, {'title': 'Confirm Delete'})

if result.button ==# 'confirm' && result.button_index == 0
    echo 'Deleted!'
endif

Search Box with History

let items = [
    \ {'type': 'input', 'name': 'pattern', 'prompt': 'Search:',
    \  'history': 'dialog_search'},
    \ {'type': 'check', 'name': 'case', 'text': 'Case &sensitive'},
    \ {'type': 'check', 'name': 'regex', 'text': 'Use &regex', 'value': 1},
    \ {'type': 'button', 'name': 'action',
    \  'items': [' &Find ', ' &Replace ', ' &Cancel ']},
    \ ]

let result = quickui#dialog#open(items, {
    \ 'title': 'Find and Replace', 'w': 50})

Label-only with Enter to Exit

let items = [
    \ {'type': 'label', 'text': [
    \   'Build completed successfully!',
    \   '',
    \   'Output: /tmp/build/output',
    \   'Time: 3.2s',
    \ ]},
    \ {'type': 'button', 'name': 'done', 'items': [' &OK ']},
    \ ]

let result = quickui#dialog#open(items, {'title': 'Build Result'})

Project Settings with Separator and Dropdown

let items = [
    \ {'type': 'input', 'name': 'project', 'prompt': 'Project:',
    \  'value': 'MyApp'},
    \ {'type': 'dropdown', 'name': 'lang', 'prompt': 'Language:',
    \  'items': ['Python', 'C/C++', 'Java', 'Go', 'Rust'], 'value': 0},
    \ {'type': 'dropdown', 'name': 'build', 'prompt': 'Build:',
    \  'items': ['Debug', 'Release', 'MinSizeRel'], 'value': 1},
    \ {'type': 'separator'},
    \ {'type': 'check', 'name': 'verbose', 'text': '&Verbose output'},
    \ {'type': 'check', 'name': 'parallel', 'text': '&Parallel build',
    \  'value': 1},
    \ {'type': 'separator'},
    \ {'type': 'button', 'name': 'action',
    \  'items': [' &Save ', ' &Cancel ']},
    \ ]

let result = quickui#dialog#open(items, {
    \ 'title': 'Project Settings', 'w': 50})

if result.button ==# 'action' && result.button_index == 0
    echo 'Project: ' . result.project
    echo 'Language: ' . result.lang        " index (0-based)
    echo 'Build: ' . result.build          " index (0-based)
    echo 'Verbose: ' . result.verbose
    echo 'Parallel: ' . result.parallel
endif

Result:

┌─ Project Settings ────────────────────────────X┐
│                                                │
│  Project:   [MyApp                          ]  │
│  Language:  [Python                        v]  │
│  Build:     [Release                       v]  │
│                                                │
├────────────────────────────────────────────────┤
│                                                │
│  [ ] Verbose output                            │
│  [x] Parallel build                            │
│                                                │
├────────────────────────────────────────────────┤
│                                                │
│            < Save >    < Cancel >              │
│                                                │
└────────────────────────────────────────────────┘

Notes

  1. Names must be unique — all controls with a name must not share the same name
  2. Multiple button rows need different names — the default name is 'button'; multiple button controls must each specify a different name
  3. Hotkeys must not conflict — the & hotkey characters across different controls must be unique
  4. button_index is 0-based — consistent with radio/check/dropdown value indexing; the first button returns 0; use button field (empty or not) to distinguish button click from Enter confirm
  5. Height limit — total control lines must not exceed screen height, otherwise an error is raised
  6. Values are preserved on cancel — after ESC cancel, the return value still contains user-modified control values, useful for restoring state when reopening