All files / cli / unstable_prompt_select.ts

92.31% Branches 12/13
98.15% Lines 53/54
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
 
 
x1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x1
x1
x1
x1
x1
 
x1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x1
x1
x1
x1
 
x17
 
x32
 
x17
x17
x17
x17
x17
x17
x17
x162
x210
x210
x17
x17
x17
x17
x17
x17
x17
x17
x68
x68
x69
x68
x70
x70
x68
x88
x88
x68
x68
x82
x68
x74
x74
x68
x76
x76
x68
 
x104
x17
 
 
 
x17























































































































































I
// Copyright 2018-2025 the Deno authors. MIT license.

import { handlePromptSelect } from "./_prompt_select.ts";

/** Options for {@linkcode promptSelect}. */
export interface PromptSelectOptions {
  /** Clear the lines after the user's input. */
  clear?: boolean;

  /** The number of lines to be visible at once */
  visibleLines?: number;

  /** The string to indicate the selected item */
  indicator?: string;
}

/**
 * Value for {@linkcode promptSelect}.
 * If an object, it must have a title and a value, else it can just be a string.
 *
 * @typeParam V The value of the underlying Entry, if any.
 */
export type PromptEntry<V = undefined> = V extends undefined ? string
  : PromptEntryWithValue<V>;

/**
 * A {@linkcode PromptEntry} with an underlying value.
 *
 * @typeParam V The value of the underlying Entry.
 */
export interface PromptEntryWithValue<V> {
  /** The label for this entry. */
  label: string;
  /** The underlying value representing this entry. */
  value: V;
}

const ETX = "\x03";
const ARROW_UP = "\u001B[A";
const ARROW_DOWN = "\u001B[B";
const CR = "\r";
const DELETE = "\u007F";

const input = Deno.stdin;

/**
 * Shows the given message and waits for the user's input. Returns the user's selected value as string.
 *
 * Also supports filtering of the options by typing.
 *
 * @typeParam V The value of the underlying Entry, if any.
 * @param message The prompt message to show to the user.
 * @param values The values for the prompt.
 * @param options The options for the prompt.
 * @returns The string that was entered or `null` if stdin is not a TTY.
 *
 * @example Basic usage
 * ```ts ignore
 * import { promptSelect } from "@std/cli/unstable-prompt-select";
 *
 * const browser = promptSelect("Please select browser", [
 *   "Chrome",
 *   "Firefox",
 *   "Safari",
 * ], { clear: true });
 * ```
 *
 * @example With title and value
 * ```ts ignore
 * import { promptSelect } from "@std/cli/unstable-prompt-select";
 *
 * const browsers = promptSelect(
 *   "Please select browsers:",
 *   [{
 *     label: "safari",
 *     value: 1,
 *   }, {
 *     label: "chrome",
 *     value: 2,
 *   }, {
 *     label: "firefox",
 *     value: 3,
 *   }],
 *   { clear: true },
 * );
 * ```
 *
 * @example With multiple options
 * ```ts ignore
 * import { promptSelect } from "@std/cli/unstable-prompt-select";
 *
 * const browser = promptSelect("What country are you from?", [
 *   "Brazil",
 *   "United States",
 *   "Japan",
 *   "China",
 *   "Canada",
 *   "Spain",
 * ], { clear: true, visibleLines: 3, indicator: "*" });
 * ```
 */
export function promptSelect<V = undefined>(
  message: string,
  values: PromptEntry<V>[],
  options: PromptSelectOptions = {},
): PromptEntry<V> | null {
  if (!input.isTerminal()) return null;

  let selectedIndex = 0;

  handlePromptSelect(
    message,
    options.indicator ?? "❯",
    values,
    options.clear,
    options.visibleLines,
    (active, absoluteIndex) => {
      if (active) {
        selectedIndex = absoluteIndex;
      }
    },
    (str, _absoluteIndex, {
      etx,
      up,
      down,
      remove,
      inputStr,
    }) => {
      switch (str) {
        case ETX:
          return etx();
        case ARROW_UP:
          up();
          break;
        case ARROW_DOWN:
          down();
          break;
        case CR:
        case " ":
          return true;
        case DELETE:
          remove();
          break;
        default:
          inputStr();
          break;
      }

      return false;
    },
  );

  return values[selectedIndex] ?? null;
}