Source file input_field.ml
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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
open! Core
module Variant = struct
module Text_state = struct
type t = { prompt : string; value : string }
let make ~prompt () = { prompt; value = "" }
let set_value t value = { t with value }
end
module Int_as_string = struct
type t = Empty | Just_minus | Positive of string | Negative of string
let make () = Empty
let injest_char c t =
match (t, c) with
| Empty, '-' -> Just_minus
| Empty, c when Char.is_digit c -> Positive (String.of_char c)
| Just_minus, '-' -> Empty
| Just_minus, c when Char.is_digit c -> Negative (String.of_char c)
| Positive string, c when Char.is_digit c ->
Positive (string ^ String.of_char c)
| Negative string, c when Char.is_digit c ->
Negative (string ^ String.of_char c)
| _, _ -> t
let injest_backspace = function
| Empty -> Empty
| Just_minus -> Empty
| Positive string ->
let new_string =
if String.is_empty string then string
else String.sub ~pos:0 ~len:(String.length string - 1) string
in
if String.is_empty new_string then Empty else Positive new_string
| Negative string ->
let new_string =
if String.is_empty string then string
else String.sub ~pos:0 ~len:(String.length string - 1) string
in
if String.is_empty new_string then Just_minus else Negative new_string
let to_string = function
| Empty -> ""
| Just_minus -> "-"
| Positive string -> string
| Negative string -> [%string "-%{string}"]
let to_int = function
| Empty -> 0
| Just_minus -> 0
| Positive string -> Int.of_string string
| Negative string -> -Int.of_string string
end
module Single_selection_state = struct
type 'a t = {
options : 'a list;
option_to_string : 'a -> string;
selected_index : int;
}
let make ~options ~option_to_string =
{ options; option_to_string; selected_index = 0 }
let maximum_index { options; _ } = List.length options - 1
let incr_selected_index t =
{ t with selected_index = min (t.selected_index + 1) (maximum_index t) }
let decr_selected_index t =
{ t with selected_index = max (t.selected_index - 1) 0 }
end
module Multi_selection_state = struct
type 'a t = {
options : 'a list;
option_to_string : 'a -> string;
selected_option_indexes : Int.Set.t;
hovered_index : int;
}
let make ~options ~option_to_string =
{
options;
option_to_string;
selected_option_indexes = Int.Set.empty;
hovered_index = 0;
}
let maximum_index { options; _ } = List.length options - 1
let incr_hovered_index t =
{ t with hovered_index = min (t.hovered_index + 1) (maximum_index t) }
let decr_hovered_index t =
{ t with hovered_index = max (t.hovered_index - 1) 0 }
let toggle_current_index
({
options = _;
option_to_string = _;
selected_option_indexes;
hovered_index;
} as t) =
if Set.mem selected_option_indexes hovered_index then
{
t with
selected_option_indexes =
Set.remove selected_option_indexes hovered_index;
}
else
{
t with
selected_option_indexes =
Set.add selected_option_indexes hovered_index;
}
end
(** [('a, 'b) t] is a variant which uses type ['a] for state and resolves with
a value of type ['b] *)
type (_, _) t =
| Any_key : (unit, unit) t
| Text : (Text_state.t, string) t
| Integer : (Int_as_string.t, int) t
| Single_selection : ('a Single_selection_state.t, 'a) t
| Multi_selection : ('a Multi_selection_state.t, 'a list) t
let render : type a b. render_info:_ -> (a, b) t -> a -> Notty.image =
fun ~render_info t state ->
let open Notty.I in
let boxed_to_screen_width =
Notty_utils.boxed
~padding_control:
(`To_min_boxed_size
(Some (render_info.Render_info.screen_width, Right), None))
in
match t with
| Any_key -> empty
| Text ->
string Theme.text_input_editable
[%string "%{state.prompt}%{state.value}"]
|> boxed_to_screen_width
| Integer ->
let as_string = Int_as_string.to_string state in
string Theme.integer_input_editable [%string "> %{as_string}"]
|> boxed_to_screen_width
| Single_selection ->
let { Single_selection_state.options; option_to_string; selected_index }
=
state
in
List.mapi options ~f:(fun index option ->
let is_selected = index = selected_index in
let attr =
if is_selected then Theme.single_selection_input_option_selected
else Theme.single_selection_input_option_not_selected
in
let text =
(if is_selected then "> " else " ") ^ option_to_string option
in
string attr text)
|>
List.rev |> vcat
| Multi_selection ->
let {
Multi_selection_state.options;
option_to_string;
selected_option_indexes;
hovered_index;
} =
state
in
List.mapi options ~f:(fun index option ->
let is_selected = Set.mem selected_option_indexes index in
let is_hovered = index = hovered_index in
let attr =
if is_hovered then Theme.multi_selection_input_option_hovered
else Theme.multi_selection_input_option_not_hovered
in
let text =
(if is_selected then "[X]" else "[ ]") ^ option_to_string option
in
string attr text)
|>
List.rev |> vcat
let injest_char : type a b. (a, b) t -> a -> _ -> a =
fun t state char ->
match (t, char) with
| Text, char ->
Text_state.set_value state (state.value ^ String.of_char char)
| Integer, char -> Int_as_string.injest_char char state
| Multi_selection, ' ' -> Multi_selection_state.toggle_current_index state
| (Any_key | Single_selection | Multi_selection), _ -> state
let injest_backspace : type a b. (a, b) t -> a -> a =
fun t state ->
match t with
| Any_key | Single_selection | Multi_selection -> state
| Text ->
let value = state.value in
if String.is_empty value then state
else
Text_state.set_value state
(String.sub ~pos:0 ~len:(String.length value - 1) value)
| Integer -> Int_as_string.injest_backspace state
let injest_arrow_key : type a b. (a, b) t -> a -> _ -> a =
fun t state direction ->
match t with
| Any_key | Text | Integer ->
state
| Single_selection -> (
match direction with
| `Left | `Right -> state
| `Up -> Single_selection_state.incr_selected_index state
| `Down -> Single_selection_state.decr_selected_index state)
| Multi_selection -> (
match direction with
| `Left | `Right -> state
| `Up -> Multi_selection_state.incr_hovered_index state
| `Down -> Multi_selection_state.decr_hovered_index state)
let to_resolvable_value : type a b. (a, b) t -> a -> b =
fun t state ->
match t with
| Any_key -> ()
| Text -> state.value
| Integer -> Int_as_string.to_int state
| Single_selection ->
let {
Single_selection_state.options;
option_to_string = _;
selected_index;
} =
state
in
List.nth_exn options selected_index
| Multi_selection ->
let {
Multi_selection_state.options;
option_to_string = _;
selected_option_indexes;
hovered_index = _;
} =
state
in
Set.to_list selected_option_indexes
|> List.map ~f:(List.nth_exn options)
end
module Unpacked = struct
type ('a, 'b) t = {
variant : ('a, 'b) Variant.t;
current_value : 'a;
resolver : 'b Lwt.u;
}
end
type t = Packed : ('a, 'b) Unpacked.t -> t
let make_any_key ~resolver () =
Packed { variant = Any_key; current_value = (); resolver }
let make_text ~prompt ~resolver () =
Packed
{
variant = Text;
current_value = Variant.Text_state.make ~prompt ();
resolver;
}
let make_integer ~resolver () =
Packed
{
variant = Integer;
current_value = Variant.Int_as_string.make ();
resolver;
}
let make_single_selection ~resolver ~options ~option_to_string () =
(if List.is_empty options then
let options_as_strings = List.map options ~f:option_to_string in
raise_s
[%message
"Options for single selection cannot be empty"
(options_as_strings : string list)]);
Packed
{
variant = Single_selection;
current_value =
Variant.Single_selection_state.make ~options ~option_to_string;
resolver;
}
let make_multi_selection ~resolver ~options ~option_to_string () =
(if List.is_empty options then
let options_as_strings = List.map options ~f:option_to_string in
raise_s
[%message
"Options for multi selection cannot be empty"
(options_as_strings : string list)]);
Packed
{
variant = Multi_selection;
current_value =
Variant.Multi_selection_state.make ~options ~option_to_string;
resolver;
}
let render ~render_info (Packed { variant; current_value; resolver = _ }) =
Variant.render ~render_info variant current_value
let injest_key_event (Packed ({ variant; current_value; resolver } as t))
(key, _mods) =
let set_up_resolver_wakeup_for_later resolver value () =
Lwt.async (fun () ->
let%lwt () =
Lwt.pause ()
in
Lwt.wakeup resolver value;
Lwt.return ())
in
match (variant, key) with
| Any_key, _ ->
set_up_resolver_wakeup_for_later resolver () ();
`Ready_to_be_destroyed
| (Text | Integer | Single_selection | Multi_selection), `ASCII c ->
let new_value = Variant.injest_char variant current_value c in
`Updated_to (Packed { t with current_value = new_value })
| (Text | Integer | Single_selection | Multi_selection), `Backspace ->
let new_value = Variant.injest_backspace variant current_value in
`Updated_to (Packed { t with current_value = new_value })
| (Text | Integer | Single_selection | Multi_selection), `Arrow direction ->
let new_value =
Variant.injest_arrow_key variant current_value direction
in
`Updated_to (Packed { t with current_value = new_value })
| (Text | Integer | Single_selection | Multi_selection), `Enter ->
set_up_resolver_wakeup_for_later resolver
(Variant.to_resolvable_value variant current_value)
();
`Ready_to_be_destroyed
| (Text | Integer | Single_selection | Multi_selection), _ ->
`Updated_to (Packed t)