cliprompts: Interactive Terminal Prompts
| Version: | 0.1.0 |
|---|
Interactive prompts for terminal applications: selection, filtered search, string and number input, enum selection, date parsing — all with correct visual cleanup on any terminal width.
Why cliprompts
- Ergonomic: Single import gives you selection, search, text, numbers, enums, and dates.
- Typed and validated: the values returned typed and user is re-prompted on errors.
- Width-aware rendering: Correctly handles terminals narrower than the displayed options. No stale characters left on screen after confirmation.
- No dependencies beyond the standard library.
- Testable: State, rendering, and input are pure functions. The mock backend records all writes and feeds queued input for deterministic tests without a TTY.
Quick start
Import and call any prompt function:
Example: cmd: -r:off
import cliprompts import std/strutils # Single selection let pick = promptSelection("Pick a colour", @["red", "green", "blue"]) doAssert 0'i16 in pick or 1'i16 in pick or 2'i16 in pick # Multi selection let picks = promptSelection("Pick colours", @["red", "green", "blue"], multi = true) # Search-filtered selection let idx = promptSearch("Find a city", @["Amsterdam", "Berlin", "Cairo", "Dublin", "Edinburgh"], maxShown = 3) # Search with mutable query storage var query = "ber" let chosen = promptSearchMut("Find or type a city", @["Amsterdam", "Berlin", "Cairo"], query, allowAny = true) # Plain string let name = promptString("Your name", default = "World") # Yes/no prompt let proceed = promptBool("Continue?", default = true) # Number input let age = promptInt("Your age", default = 18) # Enum selection with explicit type parameter type Color = enum Red, Green, Blue let col = promptEnum[Color]("Favorite color", sortAlpha = true) # Partial enum selection let opt = promptEnum[Color]("Pick an accent", {Red, Blue}, sortAlpha = true)
Implementation notes
Architecture follows the classic "state → view" pattern with a backend abstraction layer. Terminal I/O is isolated behind TerminalBackend, allowing MockTerminal for tests and real RealTerminal for production. The modular design keeps rendering logic separate from I/O and state management.
Dependencies: std/[terminal, times, strutils, algorithm, sequtils, options].
Notes on return values
- promptSelection always returns a set[int16] of selected indices. Single-select prompts therefore return a singleton set.
- promptSearch and promptSearchMut return the selected option index. promptSearchMut additionally updates the caller-provided query buffer: after a match it becomes the chosen display text; with allowAny = true and no match it remains the raw typed query.
- Search matching is case-insensitive and token-based: whitespace splits the query into tokens and every token must appear as a substring in the option text.
Procs
proc displayMessageLine(msgType: MsgType; message: string) {....raises: [IOError], tags: [WriteIOEffect], forbids: [].}
- Source Edit
proc promptBool(question: string): bool {. ...raises: [ValueError, IOError, Exception, EOFError], tags: [WriteIOEffect, RootEffect, ReadIOEffect], forbids: [].}
-
Prompts for a yes/no answer without a default.
Accepts y/n case-insensitively. Enter on empty input is rejected and re-prompts until a valid answer is entered. Esc or Ctrl+C raises IOError.
Source Edit proc promptBool(question: string; default: bool): bool {. ...raises: [ValueError, IOError, Exception, EOFError], tags: [WriteIOEffect, RootEffect, ReadIOEffect], forbids: [].}
-
Prompts for a yes/no answer.
Accepts the registered shortcut keys case-insensitively: y for yes and n for no. Pressing Enter accepts default. Esc or Ctrl+C cancels the prompt by raising IOError.
Example:
Example:
import src/backend let mock = newMockTerminal(width = 80) setBackend(mock) mock.queueInput("Y\r") doAssert promptBool("Continue?", default = false)
Source Edit proc promptDate(question: string; format: TimeFormat; default: string = ""): DateTime {. ...raises: [TimeParseError, IOError, Exception, EOFError], tags: [TimeEffect, WriteIOEffect, RootEffect, ReadIOEffect], forbids: [].}
-
Prompts for a date/time string parsed by the given format.
- question: Prompt text.
- format: Nim TimeFormat for parsing the input.
- default: Pre-filled date string.
Warning: Raises TimeParseError if default is non-empty and unparseable.Source Edit proc promptEnum[T: enum](question: string; default: T = T.low; sortAlpha: bool = false): T
-
Prompts to select a value from an enum type.
- question: Prompt text.
- default: Pre-selected value (defaults to enum's low value).
- sortAlpha: If true, displays options alphabetically by $ name.
sortAlpha changes display order only. The initial selection is still default, or T.low if you omit it.
Raises ValueError at runtime if the enum type has no selectable values.
Source Edit proc promptEnum[T: enum](question: string; options: set[T]; default: T = T.low; sortAlpha: bool = false): T
-
Prompts to select from a subset of enum values.
sortAlpha changes display order only. If default is in options, it is initially selected. Otherwise the prompt falls back to an arbitrary member of options.
Raises ValueError at runtime if options is empty.
Source Edit proc promptEnumSet[T: enum](question: string; defaults: set[T] = {}; sortAlpha: bool = false): set[T]
-
Prompts to select multiple enum values.
Returns a set of selected enum values. sortAlpha changes only the displayed order; defaults still refer to enum values, not sorted indices.
Raises ValueError at runtime if any value in defaults is not selectable.
Source Edit proc promptEnumSet[T: enum](question: string; options: set[T]; defaults: set[T] = {}; sortAlpha: bool = false): set[ T]
-
Prompts to select from a subset of enum values (multi-select).
Raises ValueError at runtime if options is empty, or if any value in defaults is not contained in options.
Source Edit proc promptFloat(question: string): float {. ...raises: [IOError, Exception, EOFError], tags: [WriteIOEffect, RootEffect, ReadIOEffect], forbids: [].}
- Convenience proc for float input without default. Source Edit
proc promptFloat(question: string; default: float = 0.0): float {. ...raises: [IOError, Exception, EOFError], tags: [WriteIOEffect, RootEffect, ReadIOEffect], forbids: [].}
- Convenience proc for float input with default. Source Edit
proc promptNumber[T: SomeNumber | range](question: string): T
-
Prompts for a number without a default.
Returns zero on empty input if T is a number, otherwise prompts until valid.
Source Edit proc promptNumber[T: SomeNumber | range](question: string; default: T): T
-
Prompts for a number with a default value.
Validates input and re-prompts on invalid entries.
Source Edit proc promptSearch[T](question: string; options: openArray[T]; initialQuery: string = ""; maxShown: int = 10): int
-
Search prompt with optional initial query.
Simplified variant with immutable initial query. Matching is case-insensitive and token-based: each whitespace-separated token in the query must occur somewhere in the option text. Returns the selected option index.
Source Edit proc promptSearchMut[T](question: string; options: openArray[T]; query: var string; maxShown: int = 10; allowAny: bool = false): int
-
Performs incremental search within options.
- question: Prompt text.
- options: Items to search through.
- query: Mutable string that stores the current search query.
- maxShown: Maximum number of results to display.
- allowAny: If true, accepts Enter even with no matching results.
Matching is case-insensitive and uses whitespace-separated query tokens. Every token must occur somewhere in the option text, so matching is an AND of substrings rather than prefix-only or fuzzy matching.
Returns index of selected item, or -1 when allowAny = true and the typed query does not match any option.
On return, query is updated to the accepted text:
- selected option text when a match was confirmed
- raw typed query when allowAny = true accepted a non-match
Example:
Example:
import src/backend let mock = newMockTerminal(width = 80) setBackend(mock) var query = "" mock.queueInput("ca\r") let idx = promptSearchMut("City", @["Amsterdam", "Cairo", "Dublin"], query) doAssert idx == 1 doAssert query == "Cairo"
Example:
import src/backend let mock = newMockTerminal(width = 80) setBackend(mock) var query = "" mock.queueInput("custom\r") let idx = promptSearchMut("Tag", @["nim", "terminal"], query, allowAny = true) doAssert idx == -1 doAssert query == "custom"
Source Edit proc promptSelection[T](question: string; options: openArray[T]; multi: bool = false; defaults: set[int16] = {}): set[ int16]
-
Displays a list of options and waits for user selection.
- question: Prompt text displayed above the options.
- options: Sequence of items to choose from. Any type with $ defined.
- multi: If true, allows multiple selections (space to toggle).
- defaults: Pre-selected indices for multi-select mode.
Returns a set of selected indices. For single selection this is still a set, usually with exactly one member.
Example:
Example:
import src/backend let mock = newMockTerminal(width = 80) setBackend(mock) mock.queueInput("\27[B\r") # Down, Enter let selected = promptSelection("Pick", @["red", "green", "blue"]) doAssert selected == {1'i16}
Source Edit proc promptString(question: string; default: string = ""): string {. ...raises: [IOError, Exception, EOFError], tags: [WriteIOEffect, RootEffect, ReadIOEffect], forbids: [].}
-
Prompts for a plain string input.
- question: Prompt text.
- default: Default value shown as placeholder.
Returns the entered string, or default if user pressed Enter without typing.
Source Edit proc promptStrings(question: string; default: openArray[string] = []): seq[ string] {....raises: [IOError, Exception, EOFError], tags: [WriteIOEffect, RootEffect, ReadIOEffect], forbids: [].}
-
Prompts for multiple string entries, terminated by empty line.
- question: Prompt text.
- default: Pre-populated list of strings.
User presses Enter on empty line to finish.
Source Edit proc setBackend(backend: TerminalBackend) {....raises: [], tags: [], forbids: [].}
-
Switches the internal backend for testing.
Use with MockTerminal to run deterministic tests without a real TTY:
Example:
import src/backend, std/strutils let mock = newMockTerminal(width = 80) setBackend(mock) mock.queueInput("abc\r") discard promptString("Name") doAssert mock.lines[0].contains("Name")
Source Edit