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
155
156
157
158
159
160
161
 
 
x1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x1
x1
x1
x1
x1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x1
x1
x1
x1
 
x18
 
x34
 
x18
x18
x18
x18
x18
x18
x18
x18
x175
x225
x225
x18
x18
x18
x18
x18
x18
x18
x18
x71
x71
x72
x71
x73
x73
x71
x92
x92
x71
x71
x86
x71
x77
x77
x71
x79
x79
x71
 
x108
x18
 
 
 
x18






























































































































































I
// Copyright 2018-2026 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;

  /**
   * If true, the visible lines will be calculated based on the remaining
   * height from the current cursor position instead of using the full
   * screen height. This is useful when you have content above the prompt
   * that should remain visible.
   */
  fitToRemainingHeight?: boolean;
}

/**
 * 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";

/**
 * 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 (!Deno.stdin.isTerminal()) return null;

  let selectedIndex = 0;

  handlePromptSelect(
    message,
    options.indicator ?? "❯",
    values,
    options.clear,
    options.visibleLines,
    options.fitToRemainingHeight,
    (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;
}